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