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;
021
022import com.fasterxml.jackson.databind.ObjectMapper;
023import java.io.IOException;
024import java.net.URLEncoder;
025import java.nio.charset.StandardCharsets;
026import java.util.Base64;
027import java.util.Base64.Encoder;
028import org.apache.hadoop.conf.Configuration;
029import org.apache.hadoop.hbase.HBaseTestingUtil;
030import org.apache.hadoop.hbase.TableName;
031import org.apache.hadoop.hbase.client.Admin;
032import org.apache.hadoop.hbase.client.ColumnFamilyDescriptor;
033import org.apache.hadoop.hbase.client.ColumnFamilyDescriptorBuilder;
034import org.apache.hadoop.hbase.client.TableDescriptorBuilder;
035import org.apache.hadoop.hbase.rest.client.Client;
036import org.apache.hadoop.hbase.rest.client.Cluster;
037import org.apache.hadoop.hbase.rest.client.Response;
038import org.apache.hadoop.hbase.rest.model.CellModel;
039import org.apache.hadoop.hbase.rest.model.CellSetModel;
040import org.apache.hadoop.hbase.rest.model.RowModel;
041import org.apache.hadoop.hbase.util.Bytes;
042import org.apache.http.Header;
043import org.apache.http.message.BasicHeader;
044import org.junit.jupiter.api.AfterAll;
045import org.junit.jupiter.api.Test;
046
047import org.apache.hbase.thirdparty.com.fasterxml.jackson.jaxrs.json.JacksonJaxbJsonProvider;
048import org.apache.hbase.thirdparty.javax.ws.rs.core.MediaType;
049
050public class MultiRowResourceTestBase {
051
052  private static final TableName TABLE = TableName.valueOf("TestRowResource");
053  private static final String CFA = "a";
054  private static final String CFB = "b";
055  private static final String COLUMN_1 = CFA + ":1";
056  private static final String COLUMN_2 = CFB + ":2";
057  private static final String ROW_1 = "testrow5";
058  private static final String VALUE_1 = "testvalue5";
059  private static final String ROW_2 = "testrow6";
060  private static final String VALUE_2 = "testvalue6";
061  private static final String TIMESTAMPED_ROW_1 = "testrow7";
062  private static final String TIMESTAMPED_ROW_2 = "testrow8";
063  private static final String TIMESTAMPED_OLD_VALUE_1 = "testvalue7-old";
064  private static final String TIMESTAMPED_NEW_VALUE_1 = "testvalue7-new";
065  private static final String TIMESTAMPED_OLD_VALUE_2 = "testvalue8-old";
066  private static final String TIMESTAMPED_NEW_VALUE_2 = "testvalue8-new";
067  private static final long TIMESTAMP_1 = 1000L;
068  private static final long TIMESTAMP_2 = 2000L;
069
070  private static final HBaseTestingUtil TEST_UTIL = new HBaseTestingUtil();
071  private static final HBaseRESTTestingUtility REST_TEST_UTIL = new HBaseRESTTestingUtility();
072
073  private static final Encoder base64UrlEncoder = java.util.Base64.getUrlEncoder().withoutPadding();
074
075  private static Client client;
076  private static Configuration conf;
077
078  private static Header extraHdr = null;
079  protected static boolean csrfEnabled = true;
080
081  protected static void initialize() throws Exception {
082    conf = TEST_UTIL.getConfiguration();
083    conf.setBoolean(RESTServer.REST_CSRF_ENABLED_KEY, csrfEnabled);
084    if (csrfEnabled) {
085      conf.set(RESTServer.REST_CSRF_BROWSER_USERAGENTS_REGEX_KEY, ".*");
086    }
087    extraHdr = new BasicHeader(RESTServer.REST_CSRF_CUSTOM_HEADER_DEFAULT, "");
088    TEST_UTIL.startMiniCluster();
089    REST_TEST_UTIL.startServletContainer(conf);
090    client = new Client(new Cluster().add("localhost", REST_TEST_UTIL.getServletPort()));
091    Admin admin = TEST_UTIL.getAdmin();
092    if (admin.tableExists(TABLE)) {
093      return;
094    }
095    TableDescriptorBuilder tableDescriptorBuilder = TableDescriptorBuilder.newBuilder(TABLE);
096    ColumnFamilyDescriptor columnFamilyDescriptor =
097      ColumnFamilyDescriptorBuilder.newBuilder(Bytes.toBytes(CFA)).build();
098    tableDescriptorBuilder.setColumnFamily(columnFamilyDescriptor);
099    columnFamilyDescriptor = ColumnFamilyDescriptorBuilder.newBuilder(Bytes.toBytes(CFB)).build();
100    tableDescriptorBuilder.setColumnFamily(columnFamilyDescriptor);
101    admin.createTable(tableDescriptorBuilder.build());
102  }
103
104  @AfterAll
105  public static void tearDownAfterAll() throws Exception {
106    REST_TEST_UTIL.shutdownServletContainer();
107    TEST_UTIL.shutdownMiniCluster();
108  }
109
110  @Test
111  public void testMultiCellGetJSON() throws IOException {
112    String row_5_url = "/" + TABLE + "/" + ROW_1 + "/" + COLUMN_1;
113    String row_6_url = "/" + TABLE + "/" + ROW_2 + "/" + COLUMN_2;
114
115    StringBuilder path = new StringBuilder();
116    path.append("/");
117    path.append(TABLE);
118    path.append("/multiget/?row=");
119    path.append(ROW_1);
120    path.append("&row=");
121    path.append(ROW_2);
122
123    if (csrfEnabled) {
124      Response response = client.post(row_5_url, Constants.MIMETYPE_BINARY, Bytes.toBytes(VALUE_1));
125      assertEquals(400, response.getCode());
126    }
127
128    client.post(row_5_url, Constants.MIMETYPE_BINARY, Bytes.toBytes(VALUE_1), extraHdr);
129    client.post(row_6_url, Constants.MIMETYPE_BINARY, Bytes.toBytes(VALUE_2), extraHdr);
130
131    Response response = client.get(path.toString(), Constants.MIMETYPE_JSON);
132    assertEquals(200, response.getCode());
133    assertEquals(Constants.MIMETYPE_JSON, response.getHeader("content-type"));
134
135    client.delete(row_5_url, extraHdr);
136    client.delete(row_6_url, extraHdr);
137  }
138
139  private void checkMultiCellGetJSON(Response response) throws IOException {
140    assertEquals(200, response.getCode());
141    assertEquals(Constants.MIMETYPE_JSON, response.getHeader("content-type"));
142
143    ObjectMapper mapper = new JacksonJaxbJsonProvider().locateMapper(CellSetModel.class,
144      MediaType.APPLICATION_JSON_TYPE);
145    CellSetModel cellSet = mapper.readValue(response.getBody(), CellSetModel.class);
146
147    RowModel rowModel = cellSet.getRows().get(0);
148    assertEquals(ROW_1, new String(rowModel.getKey()));
149    assertEquals(1, rowModel.getCells().size());
150    CellModel cell = rowModel.getCells().get(0);
151    assertEquals(COLUMN_1, new String(cell.getColumn()));
152    assertEquals(VALUE_1, new String(cell.getValue()));
153
154    rowModel = cellSet.getRows().get(1);
155    assertEquals(ROW_2, new String(rowModel.getKey()));
156    assertEquals(1, rowModel.getCells().size());
157    cell = rowModel.getCells().get(0);
158    assertEquals(COLUMN_2, new String(cell.getColumn()));
159    assertEquals(VALUE_2, new String(cell.getValue()));
160  }
161
162  // See https://issues.apache.org/jira/browse/HBASE-28174
163  @Test
164  public void testMultiCellGetJSONB64() throws IOException {
165    String row_5_url = "/" + TABLE + "/" + ROW_1 + "/" + COLUMN_1;
166    String row_6_url = "/" + TABLE + "/" + ROW_2 + "/" + COLUMN_2;
167
168    if (csrfEnabled) {
169      Response response = client.post(row_5_url, Constants.MIMETYPE_BINARY, Bytes.toBytes(VALUE_1));
170      assertEquals(400, response.getCode());
171    }
172
173    client.post(row_5_url, Constants.MIMETYPE_BINARY, Bytes.toBytes(VALUE_1), extraHdr);
174    client.post(row_6_url, Constants.MIMETYPE_BINARY, Bytes.toBytes(VALUE_2), extraHdr);
175
176    StringBuilder path = new StringBuilder();
177    Base64.Encoder encoder = Base64.getUrlEncoder().withoutPadding();
178    path.append("/");
179    path.append(TABLE);
180    path.append("/multiget/?row=");
181    path.append(encoder.encodeToString(ROW_1.getBytes(StandardCharsets.UTF_8)));
182    path.append("&row=");
183    path.append(encoder.encodeToString(ROW_2.getBytes(StandardCharsets.UTF_8)));
184    path.append("&e=b64"); // Specify encoding via query string
185
186    Response response = client.get(path.toString(), Constants.MIMETYPE_JSON);
187
188    checkMultiCellGetJSON(response);
189
190    path = new StringBuilder();
191    path.append("/");
192    path.append(TABLE);
193    path.append("/multiget/?row=");
194    path.append(encoder.encodeToString(ROW_1.getBytes(StandardCharsets.UTF_8)));
195    path.append("&row=");
196    path.append(encoder.encodeToString(ROW_2.getBytes(StandardCharsets.UTF_8)));
197
198    Header[] headers = new Header[] { new BasicHeader("Accept", Constants.MIMETYPE_JSON),
199      new BasicHeader("Encoding", "b64") // Specify encoding via header
200    };
201    response = client.get(path.toString(), headers);
202
203    checkMultiCellGetJSON(response);
204
205    client.delete(row_5_url, extraHdr);
206    client.delete(row_6_url, extraHdr);
207  }
208
209  private void postBinaryWithTimestamp(String path, String value, long timestamp)
210    throws IOException {
211    Header[] headers = new Header[] { new BasicHeader("Content-Type", Constants.MIMETYPE_BINARY),
212      new BasicHeader("X-Timestamp", Long.toString(timestamp)), extraHdr };
213    Response response = client.post(path, headers, Bytes.toBytes(value));
214    assertEquals(200, response.getCode());
215  }
216
217  @Test
218  public void testMultiCellGetWithExactTimestampJSON() throws IOException {
219    String row_7_url = "/" + TABLE + "/" + TIMESTAMPED_ROW_1 + "/" + COLUMN_1;
220    String row_8_url = "/" + TABLE + "/" + TIMESTAMPED_ROW_2 + "/" + COLUMN_2;
221    String row_7_delete_url = "/" + TABLE + "/" + TIMESTAMPED_ROW_1;
222    String row_8_delete_url = "/" + TABLE + "/" + TIMESTAMPED_ROW_2;
223
224    postBinaryWithTimestamp(row_7_url, TIMESTAMPED_OLD_VALUE_1, TIMESTAMP_1);
225    postBinaryWithTimestamp(row_7_url, TIMESTAMPED_NEW_VALUE_1, TIMESTAMP_2);
226    postBinaryWithTimestamp(row_8_url, TIMESTAMPED_OLD_VALUE_2, TIMESTAMP_1);
227    postBinaryWithTimestamp(row_8_url, TIMESTAMPED_NEW_VALUE_2, TIMESTAMP_2);
228
229    try {
230      StringBuilder path = new StringBuilder();
231      path.append("/");
232      path.append(TABLE);
233      path.append("/multiget/?row=");
234      path.append(TIMESTAMPED_ROW_1);
235      path.append("/");
236      path.append("/");
237      path.append(TIMESTAMP_1);
238      path.append("&row=");
239      path.append(TIMESTAMPED_ROW_2);
240      path.append("/");
241      path.append("/");
242      path.append(TIMESTAMP_1);
243
244      Response response = client.get(path.toString(), Constants.MIMETYPE_JSON);
245      assertEquals(200, response.getCode());
246      assertEquals(Constants.MIMETYPE_JSON, response.getHeader("content-type"));
247
248      ObjectMapper mapper = new JacksonJaxbJsonProvider().locateMapper(CellSetModel.class,
249        MediaType.APPLICATION_JSON_TYPE);
250      CellSetModel cellSet = mapper.readValue(response.getBody(), CellSetModel.class);
251
252      assertEquals(2, cellSet.getRows().size());
253
254      RowModel rowModel = cellSet.getRows().get(0);
255      assertEquals(TIMESTAMPED_ROW_1, Bytes.toString(rowModel.getKey()));
256      assertEquals(1, rowModel.getCells().size());
257      CellModel cell = rowModel.getCells().get(0);
258      assertEquals(COLUMN_1, Bytes.toString(cell.getColumn()));
259      assertEquals(TIMESTAMPED_OLD_VALUE_1, Bytes.toString(cell.getValue()));
260      assertEquals(TIMESTAMP_1, cell.getTimestamp());
261
262      rowModel = cellSet.getRows().get(1);
263      assertEquals(TIMESTAMPED_ROW_2, Bytes.toString(rowModel.getKey()));
264      assertEquals(1, rowModel.getCells().size());
265      cell = rowModel.getCells().get(0);
266      assertEquals(COLUMN_2, Bytes.toString(cell.getColumn()));
267      assertEquals(TIMESTAMPED_OLD_VALUE_2, Bytes.toString(cell.getValue()));
268      assertEquals(TIMESTAMP_1, cell.getTimestamp());
269    } finally {
270      client.delete(row_7_delete_url, extraHdr);
271      client.delete(row_8_delete_url, extraHdr);
272    }
273  }
274
275  @Test
276  public void testMultiCellGetNoKeys() throws IOException {
277    StringBuilder path = new StringBuilder();
278    path.append("/");
279    path.append(TABLE);
280    path.append("/multiget");
281
282    Response response = client.get(path.toString(), Constants.MIMETYPE_XML);
283    assertEquals(404, response.getCode());
284  }
285
286  @Test
287  public void testMultiCellGetXML() throws IOException {
288    String row_5_url = "/" + TABLE + "/" + ROW_1 + "/" + COLUMN_1;
289    String row_6_url = "/" + TABLE + "/" + ROW_2 + "/" + COLUMN_2;
290
291    StringBuilder path = new StringBuilder();
292    path.append("/");
293    path.append(TABLE);
294    path.append("/multiget/?row=");
295    path.append(ROW_1);
296    path.append("&row=");
297    path.append(ROW_2);
298
299    client.post(row_5_url, Constants.MIMETYPE_BINARY, Bytes.toBytes(VALUE_1), extraHdr);
300    client.post(row_6_url, Constants.MIMETYPE_BINARY, Bytes.toBytes(VALUE_2), extraHdr);
301
302    Response response = client.get(path.toString(), Constants.MIMETYPE_XML);
303    assertEquals(200, response.getCode());
304    assertEquals(Constants.MIMETYPE_XML, response.getHeader("content-type"));
305
306    client.delete(row_5_url, extraHdr);
307    client.delete(row_6_url, extraHdr);
308  }
309
310  @Test
311  public void testMultiCellGetWithColsJSON() throws IOException {
312    String row_5_url = "/" + TABLE + "/" + ROW_1 + "/" + COLUMN_1;
313    String row_6_url = "/" + TABLE + "/" + ROW_2 + "/" + COLUMN_2;
314
315    StringBuilder path = new StringBuilder();
316    path.append("/");
317    path.append(TABLE);
318    path.append("/multiget");
319    path.append("/" + COLUMN_1 + "," + CFB);
320    path.append("?row=");
321    path.append(ROW_1);
322    path.append("&row=");
323    path.append(ROW_2);
324
325    client.post(row_5_url, Constants.MIMETYPE_BINARY, Bytes.toBytes(VALUE_1), extraHdr);
326    client.post(row_6_url, Constants.MIMETYPE_BINARY, Bytes.toBytes(VALUE_2), extraHdr);
327
328    Response response = client.get(path.toString(), Constants.MIMETYPE_JSON);
329    assertEquals(200, response.getCode());
330    ObjectMapper mapper = new JacksonJaxbJsonProvider().locateMapper(CellSetModel.class,
331      MediaType.APPLICATION_JSON_TYPE);
332    CellSetModel cellSet = mapper.readValue(response.getBody(), CellSetModel.class);
333    assertEquals(2, cellSet.getRows().size());
334    assertEquals(ROW_1, Bytes.toString(cellSet.getRows().get(0).getKey()));
335    assertEquals(VALUE_1, Bytes.toString(cellSet.getRows().get(0).getCells().get(0).getValue()));
336    assertEquals(ROW_2, Bytes.toString(cellSet.getRows().get(1).getKey()));
337    assertEquals(VALUE_2, Bytes.toString(cellSet.getRows().get(1).getCells().get(0).getValue()));
338
339    client.delete(row_5_url, extraHdr);
340    client.delete(row_6_url, extraHdr);
341  }
342
343  @Test
344  public void testMultiCellGetJSONNotFound() throws IOException {
345    String row_5_url = "/" + TABLE + "/" + ROW_1 + "/" + COLUMN_1;
346
347    StringBuilder path = new StringBuilder();
348    path.append("/");
349    path.append(TABLE);
350    path.append("/multiget/?row=");
351    path.append(ROW_1);
352    path.append("&row=");
353    path.append(ROW_2);
354
355    client.post(row_5_url, Constants.MIMETYPE_BINARY, Bytes.toBytes(VALUE_1), extraHdr);
356    Response response = client.get(path.toString(), Constants.MIMETYPE_JSON);
357    assertEquals(200, response.getCode());
358    ObjectMapper mapper = new JacksonJaxbJsonProvider().locateMapper(CellSetModel.class,
359      MediaType.APPLICATION_JSON_TYPE);
360    CellSetModel cellSet = (CellSetModel) mapper.readValue(response.getBody(), CellSetModel.class);
361    assertEquals(1, cellSet.getRows().size());
362    assertEquals(ROW_1, Bytes.toString(cellSet.getRows().get(0).getKey()));
363    assertEquals(VALUE_1, Bytes.toString(cellSet.getRows().get(0).getCells().get(0).getValue()));
364    client.delete(row_5_url, extraHdr);
365  }
366
367  @Test
368  public void testMultiCellGetWithColsInQueryPathJSON() throws IOException {
369    String row_5_url = "/" + TABLE + "/" + ROW_1 + "/" + COLUMN_1;
370    String row_6_url = "/" + TABLE + "/" + ROW_2 + "/" + COLUMN_2;
371
372    StringBuilder path = new StringBuilder();
373    path.append("/");
374    path.append(TABLE);
375    path.append("/multiget/?row=");
376    path.append(ROW_1);
377    path.append("/");
378    path.append(COLUMN_1);
379    path.append("&row=");
380    path.append(ROW_2);
381    path.append("/");
382    path.append(COLUMN_1);
383
384    client.post(row_5_url, Constants.MIMETYPE_BINARY, Bytes.toBytes(VALUE_1), extraHdr);
385    client.post(row_6_url, Constants.MIMETYPE_BINARY, Bytes.toBytes(VALUE_2), extraHdr);
386
387    Response response = client.get(path.toString(), Constants.MIMETYPE_JSON);
388    assertEquals(200, response.getCode());
389    ObjectMapper mapper = new JacksonJaxbJsonProvider().locateMapper(CellSetModel.class,
390      MediaType.APPLICATION_JSON_TYPE);
391    CellSetModel cellSet = mapper.readValue(response.getBody(), CellSetModel.class);
392    assertEquals(1, cellSet.getRows().size());
393    assertEquals(ROW_1, Bytes.toString(cellSet.getRows().get(0).getKey()));
394    assertEquals(VALUE_1, Bytes.toString(cellSet.getRows().get(0).getCells().get(0).getValue()));
395
396    client.delete(row_5_url, extraHdr);
397    client.delete(row_6_url, extraHdr);
398  }
399
400  @Test
401  public void testMultiCellGetFilterJSON() throws IOException {
402    String row_5_url = "/" + TABLE + "/" + ROW_1 + "/" + COLUMN_1;
403    String row_6_url = "/" + TABLE + "/" + ROW_2 + "/" + COLUMN_2;
404
405    StringBuilder path = new StringBuilder();
406    path.append("/");
407    path.append(TABLE);
408    path.append("/multiget/?row=");
409    path.append(ROW_1);
410    path.append("&row=");
411    path.append(ROW_2);
412
413    if (csrfEnabled) {
414      Response response = client.post(row_5_url, Constants.MIMETYPE_BINARY, Bytes.toBytes(VALUE_1));
415      assertEquals(400, response.getCode());
416    }
417
418    client.post(row_5_url, Constants.MIMETYPE_BINARY, Bytes.toBytes(VALUE_1), extraHdr);
419    client.post(row_6_url, Constants.MIMETYPE_BINARY, Bytes.toBytes(VALUE_2), extraHdr);
420
421    Response response = client.get(path.toString(), Constants.MIMETYPE_JSON);
422    assertEquals(200, response.getCode());
423    assertEquals(Constants.MIMETYPE_JSON, response.getHeader("content-type"));
424
425    // If the filter is used, then we get the same result
426    String positivePath = path.toString() + ("&" + Constants.FILTER_B64 + "=" + base64UrlEncoder
427      .encodeToString("PrefixFilter('testrow')".getBytes(StandardCharsets.UTF_8.toString())));
428    response = client.get(positivePath, Constants.MIMETYPE_JSON);
429    checkMultiCellGetJSON(response);
430
431    // Same with non binary clean param
432    positivePath = path.toString() + ("&" + Constants.FILTER + "="
433      + URLEncoder.encode("PrefixFilter('testrow')", StandardCharsets.UTF_8.name()));
434    response = client.get(positivePath, Constants.MIMETYPE_JSON);
435    checkMultiCellGetJSON(response);
436
437    // This filter doesn't match the found rows
438    String negativePath = path.toString() + ("&" + Constants.FILTER_B64 + "=" + base64UrlEncoder
439      .encodeToString("PrefixFilter('notfound')".getBytes(StandardCharsets.UTF_8.toString())));
440    response = client.get(negativePath, Constants.MIMETYPE_JSON);
441    assertEquals(404, response.getCode());
442
443    // Same with non binary clean param
444    negativePath = path.toString() + ("&" + Constants.FILTER + "="
445      + URLEncoder.encode("PrefixFilter('notfound')", StandardCharsets.UTF_8.name()));
446    response = client.get(negativePath, Constants.MIMETYPE_JSON);
447    assertEquals(404, response.getCode());
448
449    // Check with binary parameters
450    // positive case
451    positivePath = path.toString() + ("&" + Constants.FILTER_B64 + "=" + base64UrlEncoder
452      .encodeToString(Bytes.toBytesBinary("ColumnRangeFilter ('\\x00', true, '\\xff', true)")));
453    response = client.get(positivePath, Constants.MIMETYPE_JSON);
454    checkMultiCellGetJSON(response);
455
456    // negative case
457    negativePath = path.toString() + ("&" + Constants.FILTER_B64 + "=" + base64UrlEncoder
458      .encodeToString(Bytes.toBytesBinary("ColumnRangeFilter ('\\x00', true, '1', false)")));
459    response = client.get(negativePath, Constants.MIMETYPE_JSON);
460    assertEquals(404, response.getCode());
461
462    client.delete(row_5_url, extraHdr);
463    client.delete(row_6_url, extraHdr);
464  }
465}