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