Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,7 @@ public interface Constants {
String CUSTOM_FILTERS = "hbase.rest.custom.filters";

String ROW_KEYS_PARAM_NAME = "row";
String KEY_ENCODING_QUERY_PARAM_NAME = "e";
/**
* If this query parameter is present when processing row or scanner resources, it disables server
* side block caching
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
import org.slf4j.LoggerFactory;

import org.apache.hbase.thirdparty.javax.ws.rs.GET;
import org.apache.hbase.thirdparty.javax.ws.rs.HeaderParam;
import org.apache.hbase.thirdparty.javax.ws.rs.Produces;
import org.apache.hbase.thirdparty.javax.ws.rs.core.Context;
import org.apache.hbase.thirdparty.javax.ws.rs.core.MultivaluedMap;
Expand Down Expand Up @@ -63,14 +64,18 @@ public MultiRowResource(TableResource tableResource, String versions, String col

@GET
@Produces({ MIMETYPE_XML, MIMETYPE_JSON, MIMETYPE_PROTOBUF, MIMETYPE_PROTOBUF_IETF })
public Response get(final @Context UriInfo uriInfo) {
public Response get(final @Context UriInfo uriInfo,
final @HeaderParam("Encoding") String keyEncodingHeader) {
MultivaluedMap<String, String> params = uriInfo.getQueryParameters();
String keyEncoding = (keyEncodingHeader != null)
? keyEncodingHeader
: params.getFirst(KEY_ENCODING_QUERY_PARAM_NAME);

servlet.getMetrics().incrementRequests(1);
try {
CellSetModel model = new CellSetModel();
for (String rk : params.get(ROW_KEYS_PARAM_NAME)) {
RowSpec rowSpec = new RowSpec(rk);
RowSpec rowSpec = new RowSpec(rk, keyEncoding);

if (this.versions != null) {
rowSpec.setMaxVersions(this.versions);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -72,10 +72,10 @@ public class RowResource extends ResourceBase {
* Constructor
*/
public RowResource(TableResource tableResource, String rowspec, String versions, String check,
String returnResult) throws IOException {
String returnResult, String keyEncoding) throws IOException {
super();
this.tableResource = tableResource;
this.rowspec = new RowSpec(rowspec);
this.rowspec = new RowSpec(rowspec, keyEncoding);
if (versions != null) {
this.rowspec.setMaxVersions(Integer.parseInt(versions));
}
Expand Down
33 changes: 33 additions & 0 deletions hbase-rest/src/main/java/org/apache/hadoop/hbase/rest/RowSpec.java
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
import java.io.UnsupportedEncodingException;
import java.net.URLDecoder;
import java.util.ArrayList;
import java.util.Base64;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
Expand Down Expand Up @@ -47,6 +48,10 @@ public class RowSpec {
private int maxValues = Integer.MAX_VALUE;

public RowSpec(String path) throws IllegalArgumentException {
this(path, null);
}

public RowSpec(String path, String keyEncoding) throws IllegalArgumentException {
int i = 0;
while (path.charAt(i) == '/') {
i++;
Expand All @@ -55,6 +60,34 @@ public RowSpec(String path) throws IllegalArgumentException {
i = parseColumns(path, i);
i = parseTimestamp(path, i);
i = parseQueryParams(path, i);

if (keyEncoding != null) {
// See https://en.wikipedia.org/wiki/Base64#Variants_summary_table
Base64.Decoder decoder;
switch (keyEncoding) {
case "b64":
case "base64":
case "b64url":
case "base64url":
decoder = Base64.getUrlDecoder();
break;
case "b64basic":
case "base64basic":
decoder = Base64.getDecoder();
break;
default:
throw new IllegalArgumentException("unknown key encoding '" + keyEncoding + "'");
}
this.row = decoder.decode(this.row);
if (this.endRow != null) {
this.endRow = decoder.decode(this.endRow);
}
TreeSet<byte[]> decodedColumns = new TreeSet<>(Bytes.BYTES_COMPARATOR);
for (byte[] encodedColumn : this.columns) {
decodedColumns.add(decoder.decode(encodedColumn));
}
this.columns = decodedColumns;
}
}

private int parseRowKeys(final String path, int i) throws IllegalArgumentException {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@

import org.apache.hbase.thirdparty.javax.ws.rs.DefaultValue;
import org.apache.hbase.thirdparty.javax.ws.rs.Encoded;
import org.apache.hbase.thirdparty.javax.ws.rs.HeaderParam;
import org.apache.hbase.thirdparty.javax.ws.rs.Path;
import org.apache.hbase.thirdparty.javax.ws.rs.PathParam;
import org.apache.hbase.thirdparty.javax.ws.rs.QueryParam;
Expand Down Expand Up @@ -94,9 +95,12 @@ public RowResource getRowResource(
// We need the @Encoded decorator so Jersey won't urldecode before
// the RowSpec constructor has a chance to parse
final @PathParam("rowspec") @Encoded String rowspec, final @QueryParam("v") String versions,
final @QueryParam("check") String check, final @QueryParam("rr") String returnResult)
final @QueryParam("check") String check, final @QueryParam("rr") String returnResult,
final @HeaderParam("Encoding") String keyEncodingHeader,
final @QueryParam(Constants.KEY_ENCODING_QUERY_PARAM_NAME) String keyEncodingQuery)
throws IOException {
return new RowResource(this, rowspec, versions, check, returnResult);
String keyEncoding = (keyEncodingHeader != null) ? keyEncodingHeader : keyEncodingQuery;
return new RowResource(this, rowspec, versions, check, returnResult, keyEncoding);
}

@Path("{suffixglobbingspec: .*\\*/.+}")
Expand All @@ -105,8 +109,12 @@ public RowResource getRowResourceWithSuffixGlobbing(
// the RowSpec constructor has a chance to parse
final @PathParam("suffixglobbingspec") @Encoded String suffixglobbingspec,
final @QueryParam("v") String versions, final @QueryParam("check") String check,
final @QueryParam("rr") String returnResult) throws IOException {
return new RowResource(this, suffixglobbingspec, versions, check, returnResult);
final @QueryParam("rr") String returnResult,
final @HeaderParam("Encoding") String keyEncodingHeader,
final @QueryParam(Constants.KEY_ENCODING_QUERY_PARAM_NAME) String keyEncodingQuery)
throws IOException {
String keyEncoding = (keyEncodingHeader != null) ? keyEncodingHeader : keyEncodingQuery;
return new RowResource(this, suffixglobbingspec, versions, check, returnResult, keyEncoding);
}

@Path("{scanspec: .*[*]$}")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.StringWriter;
import java.util.Base64;
import java.util.HashMap;
import java.util.Map;
import javax.xml.bind.JAXBContext;
Expand All @@ -43,6 +44,8 @@
import org.apache.hadoop.hbase.rest.model.CellSetModel;
import org.apache.hadoop.hbase.rest.model.RowModel;
import org.apache.hadoop.hbase.util.Bytes;
import org.apache.http.Header;
import org.apache.http.message.BasicHeader;
import org.junit.After;
import org.junit.AfterClass;
import org.junit.Before;
Expand Down Expand Up @@ -464,6 +467,15 @@ protected static Response getValueXML(String url) throws IOException {
return response;
}

protected static Response getValueXML(String url, Header[] headers) throws IOException {
Header[] fullHeaders = new Header[headers.length + 1];
for (int i = 0; i < headers.length; i++)
fullHeaders[i] = headers[i];
fullHeaders[headers.length] = new BasicHeader("Accept", Constants.MIMETYPE_XML);
Response response = client.get(url, fullHeaders);
return response;
}

protected static Response getValueJson(String url) throws IOException {
Response response = client.get(url, Constants.MIMETYPE_JSON);
return response;
Expand All @@ -483,6 +495,28 @@ protected static Response deleteValue(String table, String row, String column)
return response;
}

protected static Response deleteValueB64(String table, String row, String column,
boolean useQueryString) throws IOException {
StringBuilder path = new StringBuilder();
Base64.Encoder encoder = Base64.getUrlEncoder();
path.append('/');
path.append(table);
path.append('/');
path.append(encoder.encodeToString(row.getBytes("UTF-8")));
path.append('/');
path.append(encoder.encodeToString(column.getBytes("UTF-8")));

Response response;
if (useQueryString) {
path.append("?e=b64");
response = client.delete(path.toString());
} else {
response = client.delete(path.toString(), new BasicHeader("Encoding", "b64"));
}
Thread.yield();
return response;
}

protected static Response getValueXML(String table, String row, String column)
throws IOException {
StringBuilder path = new StringBuilder();
Expand All @@ -506,6 +540,26 @@ protected static Response deleteRow(String table, String row) throws IOException
return response;
}

protected static Response deleteRowB64(String table, String row, boolean useQueryString)
throws IOException {
StringBuilder path = new StringBuilder();
Base64.Encoder encoder = Base64.getUrlEncoder();
path.append('/');
path.append(table);
path.append('/');
path.append(encoder.encodeToString(row.getBytes("UTF-8")));

Response response;
if (useQueryString) {
path.append("?e=b64");
response = client.delete(path.toString());
} else {
response = client.delete(path.toString(), new BasicHeader("Encoding", "b64"));
}
Thread.yield();
return response;
}

protected static Response getValueJson(String table, String row, String column)
throws IOException {
StringBuilder path = new StringBuilder();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -100,4 +100,38 @@ public void testDeleteXML() throws IOException, JAXBException {
assertEquals(404, response.getCode());
}

private void testDeleteB64XML(boolean useQueryString) throws IOException, JAXBException {
Response response = putValueXML(TABLE, ROW_1, COLUMN_1, VALUE_1);
assertEquals(200, response.getCode());
response = putValueXML(TABLE, ROW_1, COLUMN_2, VALUE_2);
assertEquals(200, response.getCode());
checkValueXML(TABLE, ROW_1, COLUMN_1, VALUE_1);
checkValueXML(TABLE, ROW_1, COLUMN_2, VALUE_2);

response = deleteValueB64(TABLE, ROW_1, COLUMN_1, useQueryString);
assertEquals(200, response.getCode());
response = getValueXML(TABLE, ROW_1, COLUMN_1);
assertEquals(404, response.getCode());
checkValueXML(TABLE, ROW_1, COLUMN_2, VALUE_2);

response = putValueXML(TABLE, ROW_1, COLUMN_1, VALUE_1);
assertEquals(200, response.getCode());
response = checkAndDeletePB(TABLE, ROW_1, COLUMN_1, VALUE_1);
assertEquals(200, response.getCode());
response = getValueXML(TABLE, ROW_1, COLUMN_1);
assertEquals(404, response.getCode());

response = deleteRowB64(TABLE, ROW_1, useQueryString);
assertEquals(200, response.getCode());
response = getValueXML(TABLE, ROW_1, COLUMN_1);
assertEquals(404, response.getCode());
response = getValueXML(TABLE, ROW_1, COLUMN_2);
assertEquals(404, response.getCode());
}

@Test
public void testDeleteB64XML() throws IOException, JAXBException {
testDeleteB64XML(/* useQueryString: */false);
testDeleteB64XML(/* useQueryString: */true);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
import java.io.IOException;
import java.io.StringWriter;
import java.net.URLEncoder;
import java.util.Base64;
import java.util.HashMap;
import java.util.List;
import javax.xml.bind.JAXBException;
Expand All @@ -40,6 +41,7 @@
import org.apache.hadoop.hbase.testclassification.RestTests;
import org.apache.hadoop.hbase.util.Bytes;
import org.apache.http.Header;
import org.apache.http.message.BasicHeader;
import org.junit.ClassRule;
import org.junit.Test;
import org.junit.experimental.categories.Category;
Expand Down Expand Up @@ -333,6 +335,72 @@ public void testURLEncodedKey() throws IOException, JAXBException {
checkValueXML(path.toString(), TABLE, urlKey, COLUMN_1, VALUE_1);
}

private void setupValue1() throws IOException, JAXBException {
StringBuilder path = new StringBuilder();
path.append('/');
path.append(TABLE);
path.append('/');
path.append(ROW_1);
path.append('/');
path.append(COLUMN_1);
Response response = putValueXML(path.toString(), TABLE, ROW_1, COLUMN_1, VALUE_1);
assertEquals(200, response.getCode());
}

private void checkValue1(Response getResponse) throws JAXBException {
assertEquals(Constants.MIMETYPE_XML, getResponse.getHeader("content-type"));

CellSetModel cellSet =
(CellSetModel) xmlUnmarshaller.unmarshal(new ByteArrayInputStream(getResponse.getBody()));
assertEquals(1, cellSet.getRows().size());
RowModel rowModel = cellSet.getRows().get(0);
assertEquals(ROW_1, new String(rowModel.getKey()));
assertEquals(1, rowModel.getCells().size());
CellModel cell = rowModel.getCells().get(0);
assertEquals(COLUMN_1, new String(cell.getColumn()));
assertEquals(VALUE_1, new String(cell.getValue()));
}

// See https://issues.apache.org/jira/browse/HBASE-28174
@Test
public void testUrlB64EncodedKeyQueryParam() throws IOException, JAXBException {
setupValue1();

StringBuilder path = new StringBuilder();
Base64.Encoder encoder = Base64.getUrlEncoder();
path.append('/');
path.append(TABLE);
path.append('/');
path.append(encoder.encodeToString(ROW_1.getBytes("UTF-8")));
path.append('/');
path.append(encoder.encodeToString(COLUMN_1.getBytes("UTF-8")));
path.append("?e=b64");
Response response = getValueXML(path.toString());
assertEquals(200, response.getCode());

checkValue1(response);
}

// See https://issues.apache.org/jira/browse/HBASE-28174
@Test
public void testUrlB64EncodedKeyHeader() throws IOException, JAXBException {
setupValue1();

StringBuilder path = new StringBuilder();
Base64.Encoder encoder = Base64.getUrlEncoder();
path.append('/');
path.append(TABLE);
path.append('/');
path.append(encoder.encodeToString(ROW_1.getBytes("UTF-8")));
path.append('/');
path.append(encoder.encodeToString(COLUMN_1.getBytes("UTF-8")));
Response response =
getValueXML(path.toString(), new Header[] { new BasicHeader("Encoding", "b64") });
assertEquals(200, response.getCode());

checkValue1(response);
}

@Test
public void testNoSuchCF() throws IOException {
final String goodPath = "/" + TABLE + "/" + ROW_1 + "/" + CFA + ":";
Expand Down
Loading