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}