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.assertNotNull;
022import static org.junit.jupiter.api.Assertions.assertNull;
023import static org.junit.jupiter.api.Assertions.assertTrue;
024
025import java.io.ByteArrayInputStream;
026import java.io.IOException;
027import java.io.StringWriter;
028import java.util.ArrayList;
029import java.util.Iterator;
030import java.util.List;
031import java.util.Random;
032import java.util.concurrent.ThreadLocalRandom;
033import javax.xml.bind.JAXBContext;
034import javax.xml.bind.JAXBException;
035import javax.xml.bind.Marshaller;
036import javax.xml.bind.Unmarshaller;
037import org.apache.hadoop.conf.Configuration;
038import org.apache.hadoop.hbase.CellUtil;
039import org.apache.hadoop.hbase.HBaseTestingUtil;
040import org.apache.hadoop.hbase.TableName;
041import org.apache.hadoop.hbase.client.Admin;
042import org.apache.hadoop.hbase.client.ColumnFamilyDescriptor;
043import org.apache.hadoop.hbase.client.ColumnFamilyDescriptorBuilder;
044import org.apache.hadoop.hbase.client.Connection;
045import org.apache.hadoop.hbase.client.ConnectionFactory;
046import org.apache.hadoop.hbase.client.Durability;
047import org.apache.hadoop.hbase.client.Put;
048import org.apache.hadoop.hbase.client.Table;
049import org.apache.hadoop.hbase.client.TableDescriptorBuilder;
050import org.apache.hadoop.hbase.rest.client.Client;
051import org.apache.hadoop.hbase.rest.client.Cluster;
052import org.apache.hadoop.hbase.rest.client.Response;
053import org.apache.hadoop.hbase.rest.model.CellModel;
054import org.apache.hadoop.hbase.rest.model.CellSetModel;
055import org.apache.hadoop.hbase.rest.model.RowModel;
056import org.apache.hadoop.hbase.rest.model.ScannerModel;
057import org.apache.hadoop.hbase.testclassification.MediumTests;
058import org.apache.hadoop.hbase.testclassification.RestTests;
059import org.apache.hadoop.hbase.util.Bytes;
060import org.apache.http.Header;
061import org.junit.jupiter.api.AfterAll;
062import org.junit.jupiter.api.BeforeAll;
063import org.junit.jupiter.api.Tag;
064import org.junit.jupiter.api.Test;
065
066@Tag(RestTests.TAG)
067@Tag(MediumTests.TAG)
068public class TestScannerResource {
069
070  private static final TableName TABLE = TableName.valueOf("TestScannerResource");
071  private static final TableName TABLE_TO_BE_DISABLED = TableName.valueOf("ScannerResourceDisable");
072  private static final String NONEXISTENT_TABLE = "ThisTableDoesNotExist";
073  private static final String CFA = "a";
074  private static final String CFB = "b";
075  private static final String COLUMN_1 = CFA + ":1";
076  private static final String COLUMN_2 = CFB + ":2";
077
078  private static final HBaseTestingUtil TEST_UTIL = new HBaseTestingUtil();
079  private static final HBaseRESTTestingUtility REST_TEST_UTIL = new HBaseRESTTestingUtility();
080  private static Client client;
081  private static JAXBContext context;
082  private static Marshaller marshaller;
083  private static Unmarshaller unmarshaller;
084  private static int expectedRows1;
085  private static int expectedRows2;
086  private static Configuration conf;
087
088  static int insertData(Configuration conf, TableName tableName, String column, double prob)
089    throws IOException {
090    Random rng = ThreadLocalRandom.current();
091    byte[] k = new byte[3];
092    byte[][] famAndQf = CellUtil.parseColumn(Bytes.toBytes(column));
093    List<Put> puts = new ArrayList<>();
094    for (byte b1 = 'a'; b1 < 'z'; b1++) {
095      for (byte b2 = 'a'; b2 < 'z'; b2++) {
096        for (byte b3 = 'a'; b3 < 'z'; b3++) {
097          if (rng.nextDouble() < prob) {
098            k[0] = b1;
099            k[1] = b2;
100            k[2] = b3;
101            Put put = new Put(k);
102            put.setDurability(Durability.SKIP_WAL);
103            put.addColumn(famAndQf[0], famAndQf[1], k);
104            puts.add(put);
105          }
106        }
107      }
108    }
109    try (Connection conn = ConnectionFactory.createConnection(conf);
110      Table table = conn.getTable(tableName)) {
111      table.put(puts);
112    }
113    return puts.size();
114  }
115
116  static int countCellSet(CellSetModel model) {
117    int count = 0;
118    Iterator<RowModel> rows = model.getRows().iterator();
119    while (rows.hasNext()) {
120      RowModel row = rows.next();
121      Iterator<CellModel> cells = row.getCells().iterator();
122      while (cells.hasNext()) {
123        cells.next();
124        count++;
125      }
126    }
127    return count;
128  }
129
130  private static int fullTableScan(ScannerModel model) throws IOException {
131    model.setBatch(100);
132    Response response = client.put("/" + TABLE + "/scanner", Constants.MIMETYPE_PROTOBUF,
133      model.createProtobufOutput());
134    assertEquals(201, response.getCode());
135    String scannerURI = response.getLocation();
136    assertNotNull(scannerURI);
137    int count = 0;
138    while (true) {
139      response = client.get(scannerURI, Constants.MIMETYPE_PROTOBUF);
140      assertTrue(response.getCode() == 200 || response.getCode() == 204);
141      if (response.getCode() == 200) {
142        assertEquals(Constants.MIMETYPE_PROTOBUF, response.getHeader("content-type"));
143        CellSetModel cellSet = new CellSetModel();
144        cellSet.getObjectFromMessage(response.getBody());
145        Iterator<RowModel> rows = cellSet.getRows().iterator();
146        while (rows.hasNext()) {
147          RowModel row = rows.next();
148          Iterator<CellModel> cells = row.getCells().iterator();
149          while (cells.hasNext()) {
150            cells.next();
151            count++;
152          }
153        }
154      } else {
155        break;
156      }
157    }
158    // delete the scanner
159    response = client.delete(scannerURI);
160    assertEquals(200, response.getCode());
161    return count;
162  }
163
164  @BeforeAll
165  public static void setUpBeforeClass() throws Exception {
166    conf = TEST_UTIL.getConfiguration();
167    TEST_UTIL.startMiniCluster();
168    REST_TEST_UTIL.startServletContainer(conf);
169    client = new Client(new Cluster().add("localhost", REST_TEST_UTIL.getServletPort()));
170    context = JAXBContext.newInstance(CellModel.class, CellSetModel.class, RowModel.class,
171      ScannerModel.class);
172    marshaller = context.createMarshaller();
173    unmarshaller = context.createUnmarshaller();
174    Admin admin = TEST_UTIL.getAdmin();
175    if (admin.tableExists(TABLE)) {
176      return;
177    }
178    TableDescriptorBuilder tableDescriptorBuilder = TableDescriptorBuilder.newBuilder(TABLE);
179    ColumnFamilyDescriptor columnFamilyDescriptor =
180      ColumnFamilyDescriptorBuilder.newBuilder(Bytes.toBytes(CFA)).build();
181    tableDescriptorBuilder.setColumnFamily(columnFamilyDescriptor);
182    columnFamilyDescriptor = ColumnFamilyDescriptorBuilder.newBuilder(Bytes.toBytes(CFB)).build();
183    tableDescriptorBuilder.setColumnFamily(columnFamilyDescriptor);
184
185    admin.createTable(tableDescriptorBuilder.build());
186    expectedRows1 = insertData(TEST_UTIL.getConfiguration(), TABLE, COLUMN_1, 1.0);
187    expectedRows2 = insertData(TEST_UTIL.getConfiguration(), TABLE, COLUMN_2, 0.5);
188
189    tableDescriptorBuilder = TableDescriptorBuilder.newBuilder(TABLE_TO_BE_DISABLED);
190    columnFamilyDescriptor = ColumnFamilyDescriptorBuilder.newBuilder(Bytes.toBytes(CFA)).build();
191    tableDescriptorBuilder.setColumnFamily(columnFamilyDescriptor);
192    columnFamilyDescriptor = ColumnFamilyDescriptorBuilder.newBuilder(Bytes.toBytes(CFB)).build();
193    tableDescriptorBuilder.setColumnFamily(columnFamilyDescriptor);
194
195    admin.createTable(tableDescriptorBuilder.build());
196  }
197
198  @AfterAll
199  public static void tearDownAfterClass() throws Exception {
200    REST_TEST_UTIL.shutdownServletContainer();
201    TEST_UTIL.shutdownMiniCluster();
202  }
203
204  @Test
205  public void testSimpleScannerXML() throws IOException, JAXBException {
206    final int BATCH_SIZE = 5;
207    // new scanner
208    ScannerModel model = new ScannerModel();
209    model.setBatch(BATCH_SIZE);
210    model.addColumn(Bytes.toBytes(COLUMN_1));
211    StringWriter writer = new StringWriter();
212    marshaller.marshal(model, writer);
213    byte[] body = Bytes.toBytes(writer.toString());
214
215    // test put operation is forbidden in read-only mode
216    conf.set("hbase.rest.readonly", "true");
217    Response response = client.put("/" + TABLE + "/scanner", Constants.MIMETYPE_XML, body);
218    assertEquals(403, response.getCode());
219    String scannerURI = response.getLocation();
220    assertNull(scannerURI);
221
222    // recall previous put operation with read-only off
223    conf.set("hbase.rest.readonly", "false");
224    response = client.put("/" + TABLE + "/scanner", Constants.MIMETYPE_XML, body);
225    assertEquals(201, response.getCode());
226    scannerURI = response.getLocation();
227    assertNotNull(scannerURI);
228
229    // get a cell set
230    response = client.get(scannerURI, Constants.MIMETYPE_XML);
231    assertEquals(200, response.getCode());
232    assertEquals(Constants.MIMETYPE_XML, response.getHeader("content-type"));
233    CellSetModel cellSet =
234      (CellSetModel) unmarshaller.unmarshal(new ByteArrayInputStream(response.getBody()));
235    // confirm batch size conformance
236    assertEquals(BATCH_SIZE, countCellSet(cellSet));
237
238    // test delete scanner operation is forbidden in read-only mode
239    conf.set("hbase.rest.readonly", "true");
240    response = client.delete(scannerURI);
241    assertEquals(403, response.getCode());
242
243    // recall previous delete scanner operation with read-only off
244    conf.set("hbase.rest.readonly", "false");
245    response = client.delete(scannerURI);
246    assertEquals(200, response.getCode());
247  }
248
249  @Test
250  public void testSimpleScannerPB() throws IOException {
251    final int BATCH_SIZE = 10;
252    // new scanner
253    ScannerModel model = new ScannerModel();
254    model.setBatch(BATCH_SIZE);
255    model.addColumn(Bytes.toBytes(COLUMN_1));
256
257    // test put operation is forbidden in read-only mode
258    conf.set("hbase.rest.readonly", "true");
259    Response response = client.put("/" + TABLE + "/scanner", Constants.MIMETYPE_PROTOBUF,
260      model.createProtobufOutput());
261    assertEquals(403, response.getCode());
262    String scannerURI = response.getLocation();
263    assertNull(scannerURI);
264
265    // recall previous put operation with read-only off
266    conf.set("hbase.rest.readonly", "false");
267    response = client.put("/" + TABLE + "/scanner", Constants.MIMETYPE_PROTOBUF,
268      model.createProtobufOutput());
269    assertEquals(201, response.getCode());
270    scannerURI = response.getLocation();
271    assertNotNull(scannerURI);
272
273    // get a cell set
274    response = client.get(scannerURI, Constants.MIMETYPE_PROTOBUF);
275    assertEquals(200, response.getCode());
276    assertEquals(Constants.MIMETYPE_PROTOBUF, response.getHeader("content-type"));
277    CellSetModel cellSet = new CellSetModel();
278    cellSet.getObjectFromMessage(response.getBody());
279    // confirm batch size conformance
280    assertEquals(BATCH_SIZE, countCellSet(cellSet));
281
282    // test delete scanner operation is forbidden in read-only mode
283    conf.set("hbase.rest.readonly", "true");
284    response = client.delete(scannerURI);
285    assertEquals(403, response.getCode());
286
287    // recall previous delete scanner operation with read-only off
288    conf.set("hbase.rest.readonly", "false");
289    response = client.delete(scannerURI);
290    assertEquals(200, response.getCode());
291  }
292
293  @Test
294  public void testSimpleScannerBinary() throws IOException {
295    // new scanner
296    ScannerModel model = new ScannerModel();
297    model.setBatch(1);
298    model.addColumn(Bytes.toBytes(COLUMN_1));
299
300    // test put operation is forbidden in read-only mode
301    conf.set("hbase.rest.readonly", "true");
302    Response response = client.put("/" + TABLE + "/scanner", Constants.MIMETYPE_PROTOBUF,
303      model.createProtobufOutput());
304    assertEquals(403, response.getCode());
305    String scannerURI = response.getLocation();
306    assertNull(scannerURI);
307
308    // recall previous put operation with read-only off
309    conf.set("hbase.rest.readonly", "false");
310    response = client.put("/" + TABLE + "/scanner", Constants.MIMETYPE_PROTOBUF,
311      model.createProtobufOutput());
312    assertEquals(201, response.getCode());
313    scannerURI = response.getLocation();
314    assertNotNull(scannerURI);
315
316    // get a cell
317    response = client.get(scannerURI, Constants.MIMETYPE_BINARY);
318    assertEquals(200, response.getCode());
319    assertEquals(Constants.MIMETYPE_BINARY, response.getHeader("content-type"));
320    // verify that data was returned
321    assertTrue(response.getBody().length > 0);
322    // verify that the expected X-headers are present
323    boolean foundRowHeader = false, foundColumnHeader = false, foundTimestampHeader = false;
324    for (Header header : response.getHeaders()) {
325      if (header.getName().equals("X-Row")) {
326        foundRowHeader = true;
327      } else if (header.getName().equals("X-Column")) {
328        foundColumnHeader = true;
329      } else if (header.getName().equals("X-Timestamp")) {
330        foundTimestampHeader = true;
331      }
332    }
333    assertTrue(foundRowHeader);
334    assertTrue(foundColumnHeader);
335    assertTrue(foundTimestampHeader);
336
337    // test delete scanner operation is forbidden in read-only mode
338    conf.set("hbase.rest.readonly", "true");
339    response = client.delete(scannerURI);
340    assertEquals(403, response.getCode());
341
342    // recall previous delete scanner operation with read-only off
343    conf.set("hbase.rest.readonly", "false");
344    response = client.delete(scannerURI);
345    assertEquals(200, response.getCode());
346  }
347
348  @Test
349  public void testFullTableScan() throws IOException {
350    ScannerModel model = new ScannerModel();
351    model.addColumn(Bytes.toBytes(COLUMN_1));
352    assertEquals(expectedRows1, fullTableScan(model));
353
354    model = new ScannerModel();
355    model.addColumn(Bytes.toBytes(COLUMN_2));
356    assertEquals(expectedRows2, fullTableScan(model));
357  }
358
359  @Test
360  public void testTableDoesNotExist() throws IOException, JAXBException {
361    ScannerModel model = new ScannerModel();
362    StringWriter writer = new StringWriter();
363    marshaller.marshal(model, writer);
364    byte[] body = Bytes.toBytes(writer.toString());
365    Response response =
366      client.put("/" + NONEXISTENT_TABLE + "/scanner", Constants.MIMETYPE_XML, body);
367    String scannerURI = response.getLocation();
368    assertNotNull(scannerURI);
369    response = client.get(scannerURI, Constants.MIMETYPE_XML);
370    assertEquals(404, response.getCode());
371  }
372
373  @Test
374  public void testTableScanWithTableDisable() throws IOException {
375    TEST_UTIL.getAdmin().disableTable(TABLE_TO_BE_DISABLED);
376    ScannerModel model = new ScannerModel();
377    model.addColumn(Bytes.toBytes(COLUMN_1));
378    model.setCaching(1);
379    Response response = client.put("/" + TABLE_TO_BE_DISABLED + "/scanner",
380      Constants.MIMETYPE_PROTOBUF, model.createProtobufOutput());
381    // we will see the exception when we actually want to get the result.
382    assertEquals(201, response.getCode());
383    String scannerURI = response.getLocation();
384    assertNotNull(scannerURI);
385    response = client.get(scannerURI, Constants.MIMETYPE_PROTOBUF);
386    assertEquals(410, response.getCode());
387  }
388
389  @Test
390  public void deleteNonExistent() throws IOException {
391    Response response = client.delete("/" + TABLE + "/scanner/NONEXISTENT_SCAN");
392    assertEquals(404, response.getCode());
393  }
394
395  @Test
396  public void testScannerWithIncludeStartStopRowXML() throws IOException, JAXBException {
397    final int BATCH_SIZE = 5;
398    // new scanner
399    ScannerModel model = new ScannerModel();
400    // model.setBatch(BATCH_SIZE);
401    model.addColumn(Bytes.toBytes(COLUMN_1));
402    model.setStartRow(Bytes.toBytes("aaa"));
403    model.setEndRow(Bytes.toBytes("aae"));
404    StringWriter writer = new StringWriter();
405    marshaller.marshal(model, writer);
406    byte[] body = Bytes.toBytes(writer.toString());
407
408    conf.set("hbase.rest.readonly", "false");
409    Response response = client.put("/" + TABLE + "/scanner", Constants.MIMETYPE_XML, body);
410    assertEquals(201, response.getCode());
411    String scannerURI = response.getLocation();
412    assertNotNull(scannerURI);
413
414    // get a cell set
415    response = client.get(scannerURI, Constants.MIMETYPE_XML);
416    assertEquals(200, response.getCode());
417    assertEquals(Constants.MIMETYPE_XML, response.getHeader("content-type"));
418    CellSetModel cellSet =
419      (CellSetModel) unmarshaller.unmarshal(new ByteArrayInputStream(response.getBody()));
420
421    assertEquals(4, countCellSet(cellSet));
422
423    // test with include the start row false
424    model.setIncludeStartRow(false);
425    writer = new StringWriter();
426    marshaller.marshal(model, writer);
427    body = Bytes.toBytes(writer.toString());
428
429    conf.set("hbase.rest.readonly", "false");
430    response = client.put("/" + TABLE + "/scanner", Constants.MIMETYPE_XML, body);
431    assertEquals(201, response.getCode());
432    scannerURI = response.getLocation();
433    assertNotNull(scannerURI);
434
435    // get a cell set
436    response = client.get(scannerURI, Constants.MIMETYPE_XML);
437    assertEquals(200, response.getCode());
438    assertEquals(Constants.MIMETYPE_XML, response.getHeader("content-type"));
439    cellSet = (CellSetModel) unmarshaller.unmarshal(new ByteArrayInputStream(response.getBody()));
440
441    assertEquals(3, countCellSet(cellSet));
442
443    // test with include stop row true and start row false
444    model.setIncludeStartRow(false);
445    model.setIncludeStopRow(true);
446    writer = new StringWriter();
447    marshaller.marshal(model, writer);
448    body = Bytes.toBytes(writer.toString());
449
450    conf.set("hbase.rest.readonly", "false");
451    response = client.put("/" + TABLE + "/scanner", Constants.MIMETYPE_XML, body);
452    assertEquals(201, response.getCode());
453    scannerURI = response.getLocation();
454    assertNotNull(scannerURI);
455
456    // get a cell set
457    response = client.get(scannerURI, Constants.MIMETYPE_XML);
458    assertEquals(200, response.getCode());
459    assertEquals(Constants.MIMETYPE_XML, response.getHeader("content-type"));
460    cellSet = (CellSetModel) unmarshaller.unmarshal(new ByteArrayInputStream(response.getBody()));
461
462    assertEquals(4, countCellSet(cellSet));
463
464    // test with including the start row true and stop row true
465    model.setIncludeStartRow(true);
466    model.setIncludeStopRow(true);
467    writer = new StringWriter();
468    marshaller.marshal(model, writer);
469    body = Bytes.toBytes(writer.toString());
470
471    conf.set("hbase.rest.readonly", "false");
472    response = client.put("/" + TABLE + "/scanner", Constants.MIMETYPE_XML, body);
473    assertEquals(201, response.getCode());
474    scannerURI = response.getLocation();
475    assertNotNull(scannerURI);
476
477    // get a cell set
478    response = client.get(scannerURI, Constants.MIMETYPE_XML);
479    assertEquals(200, response.getCode());
480    assertEquals(Constants.MIMETYPE_XML, response.getHeader("content-type"));
481    cellSet = (CellSetModel) unmarshaller.unmarshal(new ByteArrayInputStream(response.getBody()));
482
483    assertEquals(5, countCellSet(cellSet));
484  }
485
486  @Test
487  public void testScannerWithIncludeStartStopRowPB() throws IOException {
488    final int BATCH_SIZE = 10;
489    // new scanner
490    ScannerModel model = new ScannerModel();
491    // model.setBatch(BATCH_SIZE);
492    model.addColumn(Bytes.toBytes(COLUMN_1));
493    model.setStartRow(Bytes.toBytes("aaa"));
494    model.setEndRow(Bytes.toBytes("aae"));
495
496    // test put operation is forbidden in read-only mode
497    conf.set("hbase.rest.readonly", "false");
498    Response response = client.put("/" + TABLE + "/scanner", Constants.MIMETYPE_PROTOBUF,
499      model.createProtobufOutput());
500    assertEquals(201, response.getCode());
501    String scannerURI = response.getLocation();
502    assertNotNull(scannerURI);
503
504    // get a cell set
505    response = client.get(scannerURI, Constants.MIMETYPE_PROTOBUF);
506    assertEquals(200, response.getCode());
507    assertEquals(Constants.MIMETYPE_PROTOBUF, response.getHeader("content-type"));
508    CellSetModel cellSet = new CellSetModel();
509    cellSet.getObjectFromMessage(response.getBody());
510    assertEquals(4, countCellSet(cellSet));
511
512    // test with include start row false
513    model.setIncludeStartRow(false);
514    response = client.put("/" + TABLE + "/scanner", Constants.MIMETYPE_PROTOBUF,
515      model.createProtobufOutput());
516    assertEquals(201, response.getCode());
517    scannerURI = response.getLocation();
518    assertNotNull(scannerURI);
519
520    // get a cell set
521    response = client.get(scannerURI, Constants.MIMETYPE_PROTOBUF);
522    assertEquals(200, response.getCode());
523    assertEquals(Constants.MIMETYPE_PROTOBUF, response.getHeader("content-type"));
524    cellSet = new CellSetModel();
525    cellSet.getObjectFromMessage(response.getBody());
526    assertEquals(3, countCellSet(cellSet));
527
528    // test with include stop row true
529    model.setIncludeStartRow(true);
530    model.setIncludeStopRow(true);
531    response = client.put("/" + TABLE + "/scanner", Constants.MIMETYPE_PROTOBUF,
532      model.createProtobufOutput());
533    assertEquals(201, response.getCode());
534    scannerURI = response.getLocation();
535    assertNotNull(scannerURI);
536
537    // get a cell set
538    response = client.get(scannerURI, Constants.MIMETYPE_PROTOBUF);
539    assertEquals(200, response.getCode());
540    assertEquals(Constants.MIMETYPE_PROTOBUF, response.getHeader("content-type"));
541    cellSet = new CellSetModel();
542    cellSet.getObjectFromMessage(response.getBody());
543    assertEquals(5, countCellSet(cellSet));
544  }
545}