Skip to content

Commit a6bc210

Browse files
authored
HBASE-28174 (DELETE endpoint in REST API does not support deleting binary row keys/columns
Signed-off-by: Wellington Chevreuil <wchevreuil@apache.org>
1 parent 9e74cc0 commit a6bc210

File tree

10 files changed

+357
-8
lines changed

10 files changed

+357
-8
lines changed

hbase-rest/src/main/java/org/apache/hadoop/hbase/rest/Constants.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,7 @@ public interface Constants {
8585
String CUSTOM_FILTERS = "hbase.rest.custom.filters";
8686

8787
String ROW_KEYS_PARAM_NAME = "row";
88+
String KEY_ENCODING_QUERY_PARAM_NAME = "e";
8889
/**
8990
* If this query parameter is present when processing row or scanner resources, it disables server
9091
* side block caching

hbase-rest/src/main/java/org/apache/hadoop/hbase/rest/MultiRowResource.java

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929
import org.slf4j.LoggerFactory;
3030

3131
import org.apache.hbase.thirdparty.javax.ws.rs.GET;
32+
import org.apache.hbase.thirdparty.javax.ws.rs.HeaderParam;
3233
import org.apache.hbase.thirdparty.javax.ws.rs.Produces;
3334
import org.apache.hbase.thirdparty.javax.ws.rs.core.Context;
3435
import org.apache.hbase.thirdparty.javax.ws.rs.core.MultivaluedMap;
@@ -63,14 +64,18 @@ public MultiRowResource(TableResource tableResource, String versions, String col
6364

6465
@GET
6566
@Produces({ MIMETYPE_XML, MIMETYPE_JSON, MIMETYPE_PROTOBUF, MIMETYPE_PROTOBUF_IETF })
66-
public Response get(final @Context UriInfo uriInfo) {
67+
public Response get(final @Context UriInfo uriInfo,
68+
final @HeaderParam("Encoding") String keyEncodingHeader) {
6769
MultivaluedMap<String, String> params = uriInfo.getQueryParameters();
70+
String keyEncoding = (keyEncodingHeader != null)
71+
? keyEncodingHeader
72+
: params.getFirst(KEY_ENCODING_QUERY_PARAM_NAME);
6873

6974
servlet.getMetrics().incrementRequests(1);
7075
try {
7176
CellSetModel model = new CellSetModel();
7277
for (String rk : params.get(ROW_KEYS_PARAM_NAME)) {
73-
RowSpec rowSpec = new RowSpec(rk);
78+
RowSpec rowSpec = new RowSpec(rk, keyEncoding);
7479

7580
if (this.versions != null) {
7681
rowSpec.setMaxVersions(this.versions);

hbase-rest/src/main/java/org/apache/hadoop/hbase/rest/RowResource.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -72,10 +72,10 @@ public class RowResource extends ResourceBase {
7272
* Constructor
7373
*/
7474
public RowResource(TableResource tableResource, String rowspec, String versions, String check,
75-
String returnResult) throws IOException {
75+
String returnResult, String keyEncoding) throws IOException {
7676
super();
7777
this.tableResource = tableResource;
78-
this.rowspec = new RowSpec(rowspec);
78+
this.rowspec = new RowSpec(rowspec, keyEncoding);
7979
if (versions != null) {
8080
this.rowspec.setMaxVersions(Integer.parseInt(versions));
8181
}

hbase-rest/src/main/java/org/apache/hadoop/hbase/rest/RowSpec.java

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
import java.io.UnsupportedEncodingException;
2121
import java.net.URLDecoder;
2222
import java.util.ArrayList;
23+
import java.util.Base64;
2324
import java.util.Collection;
2425
import java.util.Collections;
2526
import java.util.List;
@@ -47,6 +48,10 @@ public class RowSpec {
4748
private int maxValues = Integer.MAX_VALUE;
4849

4950
public RowSpec(String path) throws IllegalArgumentException {
51+
this(path, null);
52+
}
53+
54+
public RowSpec(String path, String keyEncoding) throws IllegalArgumentException {
5055
int i = 0;
5156
while (path.charAt(i) == '/') {
5257
i++;
@@ -55,6 +60,34 @@ public RowSpec(String path) throws IllegalArgumentException {
5560
i = parseColumns(path, i);
5661
i = parseTimestamp(path, i);
5762
i = parseQueryParams(path, i);
63+
64+
if (keyEncoding != null) {
65+
// See https://en.wikipedia.org/wiki/Base64#Variants_summary_table
66+
Base64.Decoder decoder;
67+
switch (keyEncoding) {
68+
case "b64":
69+
case "base64":
70+
case "b64url":
71+
case "base64url":
72+
decoder = Base64.getUrlDecoder();
73+
break;
74+
case "b64basic":
75+
case "base64basic":
76+
decoder = Base64.getDecoder();
77+
break;
78+
default:
79+
throw new IllegalArgumentException("unknown key encoding '" + keyEncoding + "'");
80+
}
81+
this.row = decoder.decode(this.row);
82+
if (this.endRow != null) {
83+
this.endRow = decoder.decode(this.endRow);
84+
}
85+
TreeSet<byte[]> decodedColumns = new TreeSet<>(Bytes.BYTES_COMPARATOR);
86+
for (byte[] encodedColumn : this.columns) {
87+
decodedColumns.add(decoder.decode(encodedColumn));
88+
}
89+
this.columns = decodedColumns;
90+
}
5891
}
5992

6093
private int parseRowKeys(final String path, int i) throws IllegalArgumentException {

hbase-rest/src/main/java/org/apache/hadoop/hbase/rest/TableResource.java

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@
3535

3636
import org.apache.hbase.thirdparty.javax.ws.rs.DefaultValue;
3737
import org.apache.hbase.thirdparty.javax.ws.rs.Encoded;
38+
import org.apache.hbase.thirdparty.javax.ws.rs.HeaderParam;
3839
import org.apache.hbase.thirdparty.javax.ws.rs.Path;
3940
import org.apache.hbase.thirdparty.javax.ws.rs.PathParam;
4041
import org.apache.hbase.thirdparty.javax.ws.rs.QueryParam;
@@ -94,9 +95,12 @@ public RowResource getRowResource(
9495
// We need the @Encoded decorator so Jersey won't urldecode before
9596
// the RowSpec constructor has a chance to parse
9697
final @PathParam("rowspec") @Encoded String rowspec, final @QueryParam("v") String versions,
97-
final @QueryParam("check") String check, final @QueryParam("rr") String returnResult)
98+
final @QueryParam("check") String check, final @QueryParam("rr") String returnResult,
99+
final @HeaderParam("Encoding") String keyEncodingHeader,
100+
final @QueryParam(Constants.KEY_ENCODING_QUERY_PARAM_NAME) String keyEncodingQuery)
98101
throws IOException {
99-
return new RowResource(this, rowspec, versions, check, returnResult);
102+
String keyEncoding = (keyEncodingHeader != null) ? keyEncodingHeader : keyEncodingQuery;
103+
return new RowResource(this, rowspec, versions, check, returnResult, keyEncoding);
100104
}
101105

102106
@Path("{suffixglobbingspec: .*\\*/.+}")
@@ -105,8 +109,12 @@ public RowResource getRowResourceWithSuffixGlobbing(
105109
// the RowSpec constructor has a chance to parse
106110
final @PathParam("suffixglobbingspec") @Encoded String suffixglobbingspec,
107111
final @QueryParam("v") String versions, final @QueryParam("check") String check,
108-
final @QueryParam("rr") String returnResult) throws IOException {
109-
return new RowResource(this, suffixglobbingspec, versions, check, returnResult);
112+
final @QueryParam("rr") String returnResult,
113+
final @HeaderParam("Encoding") String keyEncodingHeader,
114+
final @QueryParam(Constants.KEY_ENCODING_QUERY_PARAM_NAME) String keyEncodingQuery)
115+
throws IOException {
116+
String keyEncoding = (keyEncodingHeader != null) ? keyEncodingHeader : keyEncodingQuery;
117+
return new RowResource(this, suffixglobbingspec, versions, check, returnResult, keyEncoding);
110118
}
111119

112120
@Path("{scanspec: .*[*]$}")

hbase-rest/src/test/java/org/apache/hadoop/hbase/rest/RowResourceBase.java

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
import java.io.ByteArrayInputStream;
2424
import java.io.IOException;
2525
import java.io.StringWriter;
26+
import java.util.Base64;
2627
import java.util.HashMap;
2728
import java.util.Map;
2829
import javax.xml.bind.JAXBContext;
@@ -43,6 +44,8 @@
4344
import org.apache.hadoop.hbase.rest.model.CellSetModel;
4445
import org.apache.hadoop.hbase.rest.model.RowModel;
4546
import org.apache.hadoop.hbase.util.Bytes;
47+
import org.apache.http.Header;
48+
import org.apache.http.message.BasicHeader;
4649
import org.junit.After;
4750
import org.junit.AfterClass;
4851
import org.junit.Before;
@@ -464,6 +467,15 @@ protected static Response getValueXML(String url) throws IOException {
464467
return response;
465468
}
466469

470+
protected static Response getValueXML(String url, Header[] headers) throws IOException {
471+
Header[] fullHeaders = new Header[headers.length + 1];
472+
for (int i = 0; i < headers.length; i++)
473+
fullHeaders[i] = headers[i];
474+
fullHeaders[headers.length] = new BasicHeader("Accept", Constants.MIMETYPE_XML);
475+
Response response = client.get(url, fullHeaders);
476+
return response;
477+
}
478+
467479
protected static Response getValueJson(String url) throws IOException {
468480
Response response = client.get(url, Constants.MIMETYPE_JSON);
469481
return response;
@@ -483,6 +495,28 @@ protected static Response deleteValue(String table, String row, String column)
483495
return response;
484496
}
485497

498+
protected static Response deleteValueB64(String table, String row, String column,
499+
boolean useQueryString) throws IOException {
500+
StringBuilder path = new StringBuilder();
501+
Base64.Encoder encoder = Base64.getUrlEncoder();
502+
path.append('/');
503+
path.append(table);
504+
path.append('/');
505+
path.append(encoder.encodeToString(row.getBytes("UTF-8")));
506+
path.append('/');
507+
path.append(encoder.encodeToString(column.getBytes("UTF-8")));
508+
509+
Response response;
510+
if (useQueryString) {
511+
path.append("?e=b64");
512+
response = client.delete(path.toString());
513+
} else {
514+
response = client.delete(path.toString(), new BasicHeader("Encoding", "b64"));
515+
}
516+
Thread.yield();
517+
return response;
518+
}
519+
486520
protected static Response getValueXML(String table, String row, String column)
487521
throws IOException {
488522
StringBuilder path = new StringBuilder();
@@ -506,6 +540,26 @@ protected static Response deleteRow(String table, String row) throws IOException
506540
return response;
507541
}
508542

543+
protected static Response deleteRowB64(String table, String row, boolean useQueryString)
544+
throws IOException {
545+
StringBuilder path = new StringBuilder();
546+
Base64.Encoder encoder = Base64.getUrlEncoder();
547+
path.append('/');
548+
path.append(table);
549+
path.append('/');
550+
path.append(encoder.encodeToString(row.getBytes("UTF-8")));
551+
552+
Response response;
553+
if (useQueryString) {
554+
path.append("?e=b64");
555+
response = client.delete(path.toString());
556+
} else {
557+
response = client.delete(path.toString(), new BasicHeader("Encoding", "b64"));
558+
}
559+
Thread.yield();
560+
return response;
561+
}
562+
509563
protected static Response getValueJson(String table, String row, String column)
510564
throws IOException {
511565
StringBuilder path = new StringBuilder();

hbase-rest/src/test/java/org/apache/hadoop/hbase/rest/TestDeleteRow.java

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,4 +100,38 @@ public void testDeleteXML() throws IOException, JAXBException {
100100
assertEquals(404, response.getCode());
101101
}
102102

103+
private void testDeleteB64XML(boolean useQueryString) throws IOException, JAXBException {
104+
Response response = putValueXML(TABLE, ROW_1, COLUMN_1, VALUE_1);
105+
assertEquals(200, response.getCode());
106+
response = putValueXML(TABLE, ROW_1, COLUMN_2, VALUE_2);
107+
assertEquals(200, response.getCode());
108+
checkValueXML(TABLE, ROW_1, COLUMN_1, VALUE_1);
109+
checkValueXML(TABLE, ROW_1, COLUMN_2, VALUE_2);
110+
111+
response = deleteValueB64(TABLE, ROW_1, COLUMN_1, useQueryString);
112+
assertEquals(200, response.getCode());
113+
response = getValueXML(TABLE, ROW_1, COLUMN_1);
114+
assertEquals(404, response.getCode());
115+
checkValueXML(TABLE, ROW_1, COLUMN_2, VALUE_2);
116+
117+
response = putValueXML(TABLE, ROW_1, COLUMN_1, VALUE_1);
118+
assertEquals(200, response.getCode());
119+
response = checkAndDeletePB(TABLE, ROW_1, COLUMN_1, VALUE_1);
120+
assertEquals(200, response.getCode());
121+
response = getValueXML(TABLE, ROW_1, COLUMN_1);
122+
assertEquals(404, response.getCode());
123+
124+
response = deleteRowB64(TABLE, ROW_1, useQueryString);
125+
assertEquals(200, response.getCode());
126+
response = getValueXML(TABLE, ROW_1, COLUMN_1);
127+
assertEquals(404, response.getCode());
128+
response = getValueXML(TABLE, ROW_1, COLUMN_2);
129+
assertEquals(404, response.getCode());
130+
}
131+
132+
@Test
133+
public void testDeleteB64XML() throws IOException, JAXBException {
134+
testDeleteB64XML(/* useQueryString: */false);
135+
testDeleteB64XML(/* useQueryString: */true);
136+
}
103137
}

hbase-rest/src/test/java/org/apache/hadoop/hbase/rest/TestGetAndPutResource.java

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
import java.io.IOException;
2525
import java.io.StringWriter;
2626
import java.net.URLEncoder;
27+
import java.util.Base64;
2728
import java.util.HashMap;
2829
import java.util.List;
2930
import javax.xml.bind.JAXBException;
@@ -40,6 +41,7 @@
4041
import org.apache.hadoop.hbase.testclassification.RestTests;
4142
import org.apache.hadoop.hbase.util.Bytes;
4243
import org.apache.http.Header;
44+
import org.apache.http.message.BasicHeader;
4345
import org.junit.ClassRule;
4446
import org.junit.Test;
4547
import org.junit.experimental.categories.Category;
@@ -333,6 +335,72 @@ public void testURLEncodedKey() throws IOException, JAXBException {
333335
checkValueXML(path.toString(), TABLE, urlKey, COLUMN_1, VALUE_1);
334336
}
335337

338+
private void setupValue1() throws IOException, JAXBException {
339+
StringBuilder path = new StringBuilder();
340+
path.append('/');
341+
path.append(TABLE);
342+
path.append('/');
343+
path.append(ROW_1);
344+
path.append('/');
345+
path.append(COLUMN_1);
346+
Response response = putValueXML(path.toString(), TABLE, ROW_1, COLUMN_1, VALUE_1);
347+
assertEquals(200, response.getCode());
348+
}
349+
350+
private void checkValue1(Response getResponse) throws JAXBException {
351+
assertEquals(Constants.MIMETYPE_XML, getResponse.getHeader("content-type"));
352+
353+
CellSetModel cellSet =
354+
(CellSetModel) xmlUnmarshaller.unmarshal(new ByteArrayInputStream(getResponse.getBody()));
355+
assertEquals(1, cellSet.getRows().size());
356+
RowModel rowModel = cellSet.getRows().get(0);
357+
assertEquals(ROW_1, new String(rowModel.getKey()));
358+
assertEquals(1, rowModel.getCells().size());
359+
CellModel cell = rowModel.getCells().get(0);
360+
assertEquals(COLUMN_1, new String(cell.getColumn()));
361+
assertEquals(VALUE_1, new String(cell.getValue()));
362+
}
363+
364+
// See https://issues.apache.org/jira/browse/HBASE-28174
365+
@Test
366+
public void testUrlB64EncodedKeyQueryParam() throws IOException, JAXBException {
367+
setupValue1();
368+
369+
StringBuilder path = new StringBuilder();
370+
Base64.Encoder encoder = Base64.getUrlEncoder();
371+
path.append('/');
372+
path.append(TABLE);
373+
path.append('/');
374+
path.append(encoder.encodeToString(ROW_1.getBytes("UTF-8")));
375+
path.append('/');
376+
path.append(encoder.encodeToString(COLUMN_1.getBytes("UTF-8")));
377+
path.append("?e=b64");
378+
Response response = getValueXML(path.toString());
379+
assertEquals(200, response.getCode());
380+
381+
checkValue1(response);
382+
}
383+
384+
// See https://issues.apache.org/jira/browse/HBASE-28174
385+
@Test
386+
public void testUrlB64EncodedKeyHeader() throws IOException, JAXBException {
387+
setupValue1();
388+
389+
StringBuilder path = new StringBuilder();
390+
Base64.Encoder encoder = Base64.getUrlEncoder();
391+
path.append('/');
392+
path.append(TABLE);
393+
path.append('/');
394+
path.append(encoder.encodeToString(ROW_1.getBytes("UTF-8")));
395+
path.append('/');
396+
path.append(encoder.encodeToString(COLUMN_1.getBytes("UTF-8")));
397+
Response response =
398+
getValueXML(path.toString(), new Header[] { new BasicHeader("Encoding", "b64") });
399+
assertEquals(200, response.getCode());
400+
401+
checkValue1(response);
402+
}
403+
336404
@Test
337405
public void testNoSuchCF() throws IOException {
338406
final String goodPath = "/" + TABLE + "/" + ROW_1 + "/" + CFA + ":";

0 commit comments

Comments
 (0)