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.assertNotNull;
022import static org.junit.Assert.assertNull;
023import static org.junit.Assert.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.HBaseClassTestRule;
040import org.apache.hadoop.hbase.HBaseTestingUtil;
041import org.apache.hadoop.hbase.TableName;
042import org.apache.hadoop.hbase.client.Admin;
043import org.apache.hadoop.hbase.client.ColumnFamilyDescriptor;
044import org.apache.hadoop.hbase.client.ColumnFamilyDescriptorBuilder;
045import org.apache.hadoop.hbase.client.Connection;
046import org.apache.hadoop.hbase.client.ConnectionFactory;
047import org.apache.hadoop.hbase.client.Durability;
048import org.apache.hadoop.hbase.client.Put;
049import org.apache.hadoop.hbase.client.Table;
050import org.apache.hadoop.hbase.client.TableDescriptorBuilder;
051import org.apache.hadoop.hbase.rest.client.Client;
052import org.apache.hadoop.hbase.rest.client.Cluster;
053import org.apache.hadoop.hbase.rest.client.Response;
054import org.apache.hadoop.hbase.rest.model.CellModel;
055import org.apache.hadoop.hbase.rest.model.CellSetModel;
056import org.apache.hadoop.hbase.rest.model.RowModel;
057import org.apache.hadoop.hbase.rest.model.ScannerModel;
058import org.apache.hadoop.hbase.testclassification.MediumTests;
059import org.apache.hadoop.hbase.testclassification.RestTests;
060import org.apache.hadoop.hbase.util.Bytes;
061import org.apache.http.Header;
062import org.junit.AfterClass;
063import org.junit.BeforeClass;
064import org.junit.ClassRule;
065import org.junit.Test;
066import org.junit.experimental.categories.Category;
067import org.slf4j.Logger;
068import org.slf4j.LoggerFactory;
069
070@Category({ RestTests.class, MediumTests.class })
071public class TestScannerResource {
072
073  @ClassRule
074  public static final HBaseClassTestRule CLASS_RULE =
075    HBaseClassTestRule.forClass(TestScannerResource.class);
076
077  private static final Logger LOG = LoggerFactory.getLogger(TestScannerResource.class);
078  private static final TableName TABLE = TableName.valueOf("TestScannerResource");
079  private static final TableName TABLE_TO_BE_DISABLED = TableName.valueOf("ScannerResourceDisable");
080  private static final String NONEXISTENT_TABLE = "ThisTableDoesNotExist";
081  private static final String CFA = "a";
082  private static final String CFB = "b";
083  private static final String COLUMN_1 = CFA + ":1";
084  private static final String COLUMN_2 = CFB + ":2";
085
086  private static final HBaseTestingUtil TEST_UTIL = new HBaseTestingUtil();
087  private static final HBaseRESTTestingUtility REST_TEST_UTIL = new HBaseRESTTestingUtility();
088  private static Client client;
089  private static JAXBContext context;
090  private static Marshaller marshaller;
091  private static Unmarshaller unmarshaller;
092  private static int expectedRows1;
093  private static int expectedRows2;
094  private static Configuration conf;
095
096  static int insertData(Configuration conf, TableName tableName, String column, double prob)
097    throws IOException {
098    Random rng = ThreadLocalRandom.current();
099    byte[] k = new byte[3];
100    byte[][] famAndQf = CellUtil.parseColumn(Bytes.toBytes(column));
101    List<Put> puts = new ArrayList<>();
102    for (byte b1 = 'a'; b1 < 'z'; b1++) {
103      for (byte b2 = 'a'; b2 < 'z'; b2++) {
104        for (byte b3 = 'a'; b3 < 'z'; b3++) {
105          if (rng.nextDouble() < prob) {
106            k[0] = b1;
107            k[1] = b2;
108            k[2] = b3;
109            Put put = new Put(k);
110            put.setDurability(Durability.SKIP_WAL);
111            put.addColumn(famAndQf[0], famAndQf[1], k);
112            puts.add(put);
113          }
114        }
115      }
116    }
117    try (Connection conn = ConnectionFactory.createConnection(conf);
118      Table table = conn.getTable(tableName)) {
119      table.put(puts);
120    }
121    return puts.size();
122  }
123
124  static int countCellSet(CellSetModel model) {
125    int count = 0;
126    Iterator<RowModel> rows = model.getRows().iterator();
127    while (rows.hasNext()) {
128      RowModel row = rows.next();
129      Iterator<CellModel> cells = row.getCells().iterator();
130      while (cells.hasNext()) {
131        cells.next();
132        count++;
133      }
134    }
135    return count;
136  }
137
138  private static int fullTableScan(ScannerModel model) throws IOException {
139    model.setBatch(100);
140    Response response = client.put("/" + TABLE + "/scanner", Constants.MIMETYPE_PROTOBUF,
141      model.createProtobufOutput());
142    assertEquals(201, response.getCode());
143    String scannerURI = response.getLocation();
144    assertNotNull(scannerURI);
145    int count = 0;
146    while (true) {
147      response = client.get(scannerURI, Constants.MIMETYPE_PROTOBUF);
148      assertTrue(response.getCode() == 200 || response.getCode() == 204);
149      if (response.getCode() == 200) {
150        assertEquals(Constants.MIMETYPE_PROTOBUF, response.getHeader("content-type"));
151        CellSetModel cellSet = new CellSetModel();
152        cellSet.getObjectFromMessage(response.getBody());
153        Iterator<RowModel> rows = cellSet.getRows().iterator();
154        while (rows.hasNext()) {
155          RowModel row = rows.next();
156          Iterator<CellModel> cells = row.getCells().iterator();
157          while (cells.hasNext()) {
158            cells.next();
159            count++;
160          }
161        }
162      } else {
163        break;
164      }
165    }
166    // delete the scanner
167    response = client.delete(scannerURI);
168    assertEquals(200, response.getCode());
169    return count;
170  }
171
172  @BeforeClass
173  public static void setUpBeforeClass() throws Exception {
174    conf = TEST_UTIL.getConfiguration();
175    TEST_UTIL.startMiniCluster();
176    REST_TEST_UTIL.startServletContainer(conf);
177    client = new Client(new Cluster().add("localhost", REST_TEST_UTIL.getServletPort()));
178    context = JAXBContext.newInstance(CellModel.class, CellSetModel.class, RowModel.class,
179      ScannerModel.class);
180    marshaller = context.createMarshaller();
181    unmarshaller = context.createUnmarshaller();
182    Admin admin = TEST_UTIL.getAdmin();
183    if (admin.tableExists(TABLE)) {
184      return;
185    }
186    TableDescriptorBuilder tableDescriptorBuilder = TableDescriptorBuilder.newBuilder(TABLE);
187    ColumnFamilyDescriptor columnFamilyDescriptor =
188      ColumnFamilyDescriptorBuilder.newBuilder(Bytes.toBytes(CFA)).build();
189    tableDescriptorBuilder.setColumnFamily(columnFamilyDescriptor);
190    columnFamilyDescriptor = ColumnFamilyDescriptorBuilder.newBuilder(Bytes.toBytes(CFB)).build();
191    tableDescriptorBuilder.setColumnFamily(columnFamilyDescriptor);
192
193    admin.createTable(tableDescriptorBuilder.build());
194    expectedRows1 = insertData(TEST_UTIL.getConfiguration(), TABLE, COLUMN_1, 1.0);
195    expectedRows2 = insertData(TEST_UTIL.getConfiguration(), TABLE, COLUMN_2, 0.5);
196
197    tableDescriptorBuilder = TableDescriptorBuilder.newBuilder(TABLE_TO_BE_DISABLED);
198    columnFamilyDescriptor = ColumnFamilyDescriptorBuilder.newBuilder(Bytes.toBytes(CFA)).build();
199    tableDescriptorBuilder.setColumnFamily(columnFamilyDescriptor);
200    columnFamilyDescriptor = ColumnFamilyDescriptorBuilder.newBuilder(Bytes.toBytes(CFB)).build();
201    tableDescriptorBuilder.setColumnFamily(columnFamilyDescriptor);
202
203    admin.createTable(tableDescriptorBuilder.build());
204  }
205
206  @AfterClass
207  public static void tearDownAfterClass() throws Exception {
208    REST_TEST_UTIL.shutdownServletContainer();
209    TEST_UTIL.shutdownMiniCluster();
210  }
211
212  @Test
213  public void testSimpleScannerXML() throws IOException, JAXBException {
214    final int BATCH_SIZE = 5;
215    // new scanner
216    ScannerModel model = new ScannerModel();
217    model.setBatch(BATCH_SIZE);
218    model.addColumn(Bytes.toBytes(COLUMN_1));
219    StringWriter writer = new StringWriter();
220    marshaller.marshal(model, writer);
221    byte[] body = Bytes.toBytes(writer.toString());
222
223    // test put operation is forbidden in read-only mode
224    conf.set("hbase.rest.readonly", "true");
225    Response response = client.put("/" + TABLE + "/scanner", Constants.MIMETYPE_XML, body);
226    assertEquals(403, response.getCode());
227    String scannerURI = response.getLocation();
228    assertNull(scannerURI);
229
230    // recall previous put operation with read-only off
231    conf.set("hbase.rest.readonly", "false");
232    response = client.put("/" + TABLE + "/scanner", Constants.MIMETYPE_XML, body);
233    assertEquals(201, response.getCode());
234    scannerURI = response.getLocation();
235    assertNotNull(scannerURI);
236
237    // get a cell set
238    response = client.get(scannerURI, Constants.MIMETYPE_XML);
239    assertEquals(200, response.getCode());
240    assertEquals(Constants.MIMETYPE_XML, response.getHeader("content-type"));
241    CellSetModel cellSet =
242      (CellSetModel) unmarshaller.unmarshal(new ByteArrayInputStream(response.getBody()));
243    // confirm batch size conformance
244    assertEquals(BATCH_SIZE, countCellSet(cellSet));
245
246    // test delete scanner operation is forbidden in read-only mode
247    conf.set("hbase.rest.readonly", "true");
248    response = client.delete(scannerURI);
249    assertEquals(403, response.getCode());
250
251    // recall previous delete scanner operation with read-only off
252    conf.set("hbase.rest.readonly", "false");
253    response = client.delete(scannerURI);
254    assertEquals(200, response.getCode());
255  }
256
257  @Test
258  public void testSimpleScannerPB() throws IOException {
259    final int BATCH_SIZE = 10;
260    // new scanner
261    ScannerModel model = new ScannerModel();
262    model.setBatch(BATCH_SIZE);
263    model.addColumn(Bytes.toBytes(COLUMN_1));
264
265    // test put operation is forbidden in read-only mode
266    conf.set("hbase.rest.readonly", "true");
267    Response response = client.put("/" + TABLE + "/scanner", Constants.MIMETYPE_PROTOBUF,
268      model.createProtobufOutput());
269    assertEquals(403, response.getCode());
270    String scannerURI = response.getLocation();
271    assertNull(scannerURI);
272
273    // recall previous put operation with read-only off
274    conf.set("hbase.rest.readonly", "false");
275    response = client.put("/" + TABLE + "/scanner", Constants.MIMETYPE_PROTOBUF,
276      model.createProtobufOutput());
277    assertEquals(201, response.getCode());
278    scannerURI = response.getLocation();
279    assertNotNull(scannerURI);
280
281    // get a cell set
282    response = client.get(scannerURI, Constants.MIMETYPE_PROTOBUF);
283    assertEquals(200, response.getCode());
284    assertEquals(Constants.MIMETYPE_PROTOBUF, response.getHeader("content-type"));
285    CellSetModel cellSet = new CellSetModel();
286    cellSet.getObjectFromMessage(response.getBody());
287    // confirm batch size conformance
288    assertEquals(BATCH_SIZE, countCellSet(cellSet));
289
290    // test delete scanner operation is forbidden in read-only mode
291    conf.set("hbase.rest.readonly", "true");
292    response = client.delete(scannerURI);
293    assertEquals(403, response.getCode());
294
295    // recall previous delete scanner operation with read-only off
296    conf.set("hbase.rest.readonly", "false");
297    response = client.delete(scannerURI);
298    assertEquals(200, response.getCode());
299  }
300
301  @Test
302  public void testSimpleScannerBinary() throws IOException {
303    // new scanner
304    ScannerModel model = new ScannerModel();
305    model.setBatch(1);
306    model.addColumn(Bytes.toBytes(COLUMN_1));
307
308    // test put operation is forbidden in read-only mode
309    conf.set("hbase.rest.readonly", "true");
310    Response response = client.put("/" + TABLE + "/scanner", Constants.MIMETYPE_PROTOBUF,
311      model.createProtobufOutput());
312    assertEquals(403, response.getCode());
313    String scannerURI = response.getLocation();
314    assertNull(scannerURI);
315
316    // recall previous put operation with read-only off
317    conf.set("hbase.rest.readonly", "false");
318    response = client.put("/" + TABLE + "/scanner", Constants.MIMETYPE_PROTOBUF,
319      model.createProtobufOutput());
320    assertEquals(201, response.getCode());
321    scannerURI = response.getLocation();
322    assertNotNull(scannerURI);
323
324    // get a cell
325    response = client.get(scannerURI, Constants.MIMETYPE_BINARY);
326    assertEquals(200, response.getCode());
327    assertEquals(Constants.MIMETYPE_BINARY, response.getHeader("content-type"));
328    // verify that data was returned
329    assertTrue(response.getBody().length > 0);
330    // verify that the expected X-headers are present
331    boolean foundRowHeader = false, foundColumnHeader = false, foundTimestampHeader = false;
332    for (Header header : response.getHeaders()) {
333      if (header.getName().equals("X-Row")) {
334        foundRowHeader = true;
335      } else if (header.getName().equals("X-Column")) {
336        foundColumnHeader = true;
337      } else if (header.getName().equals("X-Timestamp")) {
338        foundTimestampHeader = true;
339      }
340    }
341    assertTrue(foundRowHeader);
342    assertTrue(foundColumnHeader);
343    assertTrue(foundTimestampHeader);
344
345    // test delete scanner operation is forbidden in read-only mode
346    conf.set("hbase.rest.readonly", "true");
347    response = client.delete(scannerURI);
348    assertEquals(403, response.getCode());
349
350    // recall previous delete scanner operation with read-only off
351    conf.set("hbase.rest.readonly", "false");
352    response = client.delete(scannerURI);
353    assertEquals(200, response.getCode());
354  }
355
356  @Test
357  public void testFullTableScan() throws IOException {
358    ScannerModel model = new ScannerModel();
359    model.addColumn(Bytes.toBytes(COLUMN_1));
360    assertEquals(expectedRows1, fullTableScan(model));
361
362    model = new ScannerModel();
363    model.addColumn(Bytes.toBytes(COLUMN_2));
364    assertEquals(expectedRows2, fullTableScan(model));
365  }
366
367  @Test
368  public void testTableDoesNotExist() throws IOException, JAXBException {
369    ScannerModel model = new ScannerModel();
370    StringWriter writer = new StringWriter();
371    marshaller.marshal(model, writer);
372    byte[] body = Bytes.toBytes(writer.toString());
373    Response response =
374      client.put("/" + NONEXISTENT_TABLE + "/scanner", Constants.MIMETYPE_XML, body);
375    String scannerURI = response.getLocation();
376    assertNotNull(scannerURI);
377    response = client.get(scannerURI, Constants.MIMETYPE_XML);
378    assertEquals(404, response.getCode());
379  }
380
381  @Test
382  public void testTableScanWithTableDisable() throws IOException {
383    TEST_UTIL.getAdmin().disableTable(TABLE_TO_BE_DISABLED);
384    ScannerModel model = new ScannerModel();
385    model.addColumn(Bytes.toBytes(COLUMN_1));
386    model.setCaching(1);
387    Response response = client.put("/" + TABLE_TO_BE_DISABLED + "/scanner",
388      Constants.MIMETYPE_PROTOBUF, model.createProtobufOutput());
389    // we will see the exception when we actually want to get the result.
390    assertEquals(201, response.getCode());
391    String scannerURI = response.getLocation();
392    assertNotNull(scannerURI);
393    response = client.get(scannerURI, Constants.MIMETYPE_PROTOBUF);
394    assertEquals(410, response.getCode());
395  }
396
397  @Test
398  public void deleteNonExistent() throws IOException {
399    Response response = client.delete("/" + TABLE + "/scanner/NONEXISTENT_SCAN");
400    assertEquals(404, response.getCode());
401  }
402}