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