Index: src/test/java/org/apache/hadoop/hbase/rest/TestRowResource.java =================================================================== --- src/test/java/org/apache/hadoop/hbase/rest/TestRowResource.java (revision 1215035) +++ src/test/java/org/apache/hadoop/hbase/rest/TestRowResource.java (working copy) @@ -66,8 +66,7 @@ private static final String VALUE_4 = "testvalue4"; private static final HBaseTestingUtility TEST_UTIL = new HBaseTestingUtility(); - private static final HBaseRESTTestingUtility REST_TEST_UTIL = - new HBaseRESTTestingUtility(); + private static final HBaseRESTTestingUtility REST_TEST_UTIL = new HBaseRESTTestingUtility(); private static Client client; private static JAXBContext context; private static Marshaller marshaller; @@ -79,14 +78,12 @@ conf = TEST_UTIL.getConfiguration(); TEST_UTIL.startMiniCluster(3); REST_TEST_UTIL.startServletContainer(conf); - context = JAXBContext.newInstance( - CellModel.class, - CellSetModel.class, + context = JAXBContext.newInstance(CellModel.class, CellSetModel.class, RowModel.class); marshaller = context.createMarshaller(); unmarshaller = context.createUnmarshaller(); - client = new Client(new Cluster().add("localhost", - REST_TEST_UTIL.getServletPort())); + client = new Client(new Cluster().add("localhost", + REST_TEST_UTIL.getServletPort())); HBaseAdmin admin = TEST_UTIL.getHBaseAdmin(); if (admin.tableExists(TABLE)) { return; @@ -103,7 +100,7 @@ TEST_UTIL.shutdownMiniCluster(); } - private static Response deleteRow(String table, String row) + private static Response deleteRow(String table, String row) throws IOException { StringBuilder path = new StringBuilder(); path.append('/'); @@ -160,7 +157,7 @@ return response; } - private static Response getValuePB(String table, String row, String column) + private static Response getValuePB(String table, String row, String column) throws IOException { StringBuilder path = new StringBuilder(); path.append('/'); @@ -173,7 +170,7 @@ } private static Response getValuePB(String url) throws IOException { - Response response = client.get(url, Constants.MIMETYPE_PROTOBUF); + Response response = client.get(url, Constants.MIMETYPE_PROTOBUF); return response; } @@ -192,24 +189,88 @@ private static Response putValueXML(String url, String table, String row, String column, String value) throws IOException, JAXBException { RowModel rowModel = new RowModel(row); - rowModel.addCell(new CellModel(Bytes.toBytes(column), - Bytes.toBytes(value))); + rowModel + .addCell(new CellModel(Bytes.toBytes(column), Bytes.toBytes(value))); CellSetModel cellSetModel = new CellSetModel(); cellSetModel.addRow(rowModel); StringWriter writer = new StringWriter(); marshaller.marshal(cellSetModel, writer); Response response = client.put(url, Constants.MIMETYPE_XML, - Bytes.toBytes(writer.toString())); + Bytes.toBytes(writer.toString())); Thread.yield(); return response; } + private static Response checkAndPutValueXML(String url, String table, + String row, String column, String valueToCheck, String valueToPut) + throws IOException, JAXBException { + RowModel rowModel = new RowModel(row); + rowModel.addCell(new CellModel(Bytes.toBytes(column), Bytes + .toBytes(valueToPut))); + rowModel.addCell(new CellModel(Bytes.toBytes(column), Bytes + .toBytes(valueToCheck))); + CellSetModel cellSetModel = new CellSetModel(); + cellSetModel.addRow(rowModel); + StringWriter writer = new StringWriter(); + marshaller.marshal(cellSetModel, writer); + Response response = client.put(url, Constants.MIMETYPE_XML, + Bytes.toBytes(writer.toString())); + Thread.yield(); + return response; + } + + private static Response checkAndPutValueXML(String table, String row, + String column, String valueToCheck, String valueToPut) + throws IOException, JAXBException { + StringBuilder path = new StringBuilder(); + path.append('/'); + path.append("checkandput"); + path.append('/'); + path.append(table); + path.append('/'); + path.append(row); + path.append('/'); + path.append(column); + return checkAndPutValueXML(path.toString(), table, row, column, + valueToCheck, valueToPut); + } + + private static Response checkAndDeleteXML(String url, String table, + String row, String column, String valueToCheck) throws IOException, + JAXBException { + RowModel rowModel = new RowModel(row); + rowModel.addCell(new CellModel(Bytes.toBytes(column), Bytes + .toBytes(valueToCheck))); + CellSetModel cellSetModel = new CellSetModel(); + cellSetModel.addRow(rowModel); + StringWriter writer = new StringWriter(); + marshaller.marshal(cellSetModel, writer); + Response response = client.put(url, Constants.MIMETYPE_XML, + Bytes.toBytes(writer.toString())); + Thread.yield(); + return response; + } + + private static Response checkAndDeleteXML(String table, String row, + String column, String valueToCheck) throws IOException, JAXBException { + StringBuilder path = new StringBuilder(); + path.append('/'); + path.append("checkanddelete"); + path.append('/'); + path.append(table); + path.append('/'); + path.append(row); + path.append('/'); + path.append(column); + return checkAndDeleteXML(path.toString(), table, row, column, valueToCheck); + } + private static void checkValueXML(String table, String row, String column, String value) throws IOException, JAXBException { Response response = getValueXML(table, row, column); assertEquals(response.getCode(), 200); - CellSetModel cellSet = (CellSetModel) - unmarshaller.unmarshal(new ByteArrayInputStream(response.getBody())); + CellSetModel cellSet = (CellSetModel) unmarshaller + .unmarshal(new ByteArrayInputStream(response.getBody())); RowModel rowModel = cellSet.getRows().get(0); CellModel cell = rowModel.getCells().get(0); assertEquals(Bytes.toString(cell.getColumn()), column); @@ -220,8 +281,8 @@ String column, String value) throws IOException, JAXBException { Response response = getValueXML(url); assertEquals(response.getCode(), 200); - CellSetModel cellSet = (CellSetModel) - unmarshaller.unmarshal(new ByteArrayInputStream(response.getBody())); + CellSetModel cellSet = (CellSetModel) unmarshaller + .unmarshal(new ByteArrayInputStream(response.getBody())); RowModel rowModel = cellSet.getRows().get(0); CellModel cell = rowModel.getCells().get(0); assertEquals(Bytes.toString(cell.getColumn()), column); @@ -243,17 +304,62 @@ private static Response putValuePB(String url, String table, String row, String column, String value) throws IOException { RowModel rowModel = new RowModel(row); - rowModel.addCell(new CellModel(Bytes.toBytes(column), - Bytes.toBytes(value))); + rowModel + .addCell(new CellModel(Bytes.toBytes(column), Bytes.toBytes(value))); CellSetModel cellSetModel = new CellSetModel(); cellSetModel.addRow(rowModel); Response response = client.put(url, Constants.MIMETYPE_PROTOBUF, - cellSetModel.createProtobufOutput()); + cellSetModel.createProtobufOutput()); Thread.yield(); return response; } - private static void checkValuePB(String table, String row, String column, + private static Response checkAndPutValuePB(String url, String table, + String row, String column, String valueToCheck, String valueToPut) + throws IOException { + RowModel rowModel = new RowModel(row); + rowModel.addCell(new CellModel(Bytes.toBytes(column), Bytes + .toBytes(valueToPut))); + rowModel.addCell(new CellModel(Bytes.toBytes(column), Bytes + .toBytes(valueToCheck))); + CellSetModel cellSetModel = new CellSetModel(); + cellSetModel.addRow(rowModel); + Response response = client.put(url, Constants.MIMETYPE_PROTOBUF, + cellSetModel.createProtobufOutput()); + Thread.yield(); + return response; + } + + private static Response checkAndPutValuePB(String table, String row, + String column, String valueToCheck, String valueToPut) throws IOException { + StringBuilder path = new StringBuilder(); + path.append('/'); + path.append("checkandput"); + path.append('/'); + path.append(table); + path.append('/'); + path.append(row); + path.append('/'); + path.append(column); + return checkAndPutValuePB(path.toString(), table, row, column, + valueToCheck, valueToPut); + } + + private static Response checkAndDeletePB(String table, String row, + String column, String value) throws IOException { + StringBuilder path = new StringBuilder(); + path.append('/'); + path.append("checkanddelete"); + path.append('/'); + path.append(table); + path.append('/'); + path.append(row); + path.append('/'); + path.append(column); + return checkAndPutValuePB(path.toString(), table, row, column, value, value); + } + + private static void checkValuePB(String table, String row, String column, String value) throws IOException { Response response = getValuePB(table, row, column); assertEquals(response.getCode(), 200); @@ -268,7 +374,7 @@ @Test public void testDelete() throws IOException, JAXBException { Response response; - + response = putValueXML(TABLE, ROW_1, COLUMN_1, VALUE_1); assertEquals(response.getCode(), 200); response = putValueXML(TABLE, ROW_1, COLUMN_2, VALUE_2); @@ -282,8 +388,15 @@ assertEquals(response.getCode(), 404); checkValueXML(TABLE, ROW_1, COLUMN_2, VALUE_2); + response = putValueXML(TABLE, ROW_1, COLUMN_1, VALUE_1); + assertEquals(response.getCode(), 200); + response = checkAndDeletePB(TABLE, ROW_1, COLUMN_1, VALUE_1); + assertEquals(response.getCode(), 200); + response = getValueXML(TABLE, ROW_1, COLUMN_1); + assertEquals(response.getCode(), 404); + response = deleteRow(TABLE, ROW_1); - assertEquals(response.getCode(), 200); + assertEquals(response.getCode(), 200); response = getValueXML(TABLE, ROW_1, COLUMN_1); assertEquals(response.getCode(), 404); response = getValueXML(TABLE, ROW_1, COLUMN_2); @@ -300,8 +413,14 @@ assertEquals(response.getCode(), 403); response = putValuePB(TABLE, ROW_1, COLUMN_1, VALUE_1); assertEquals(response.getCode(), 403); + response = checkAndPutValueXML(TABLE, ROW_1, COLUMN_1, VALUE_1, VALUE_2); + assertEquals(response.getCode(), 403); + response = checkAndPutValuePB(TABLE, ROW_1, COLUMN_1, VALUE_1, VALUE_2); + assertEquals(response.getCode(), 403); response = deleteValue(TABLE, ROW_1, COLUMN_1); assertEquals(response.getCode(), 403); + response = checkAndDeletePB(TABLE, ROW_1, COLUMN_1, VALUE_1); + assertEquals(response.getCode(), 403); response = deleteRow(TABLE, ROW_1); assertEquals(response.getCode(), 403); @@ -311,6 +430,10 @@ assertEquals(response.getCode(), 200); response = putValuePB(TABLE, ROW_1, COLUMN_1, VALUE_1); assertEquals(response.getCode(), 200); + response = checkAndPutValueXML(TABLE, ROW_1, COLUMN_1, VALUE_1, VALUE_2); + assertEquals(response.getCode(), 200); + response = checkAndPutValuePB(TABLE, ROW_1, COLUMN_1, VALUE_2, VALUE_3); + assertEquals(response.getCode(), 200); response = deleteValue(TABLE, ROW_1, COLUMN_1); assertEquals(response.getCode(), 200); response = deleteRow(TABLE, ROW_1); @@ -328,7 +451,13 @@ response = putValueXML(TABLE, ROW_1, COLUMN_1, VALUE_2); assertEquals(response.getCode(), 200); checkValueXML(TABLE, ROW_1, COLUMN_1, VALUE_2); + response = checkAndPutValueXML(TABLE, ROW_1, COLUMN_1, VALUE_2, VALUE_3); + assertEquals(response.getCode(), 200); + checkValueXML(TABLE, ROW_1, COLUMN_1, VALUE_3); + response = checkAndDeleteXML(TABLE, ROW_1, COLUMN_1, VALUE_3); + assertEquals(response.getCode(), 200); + response = deleteRow(TABLE, ROW_1); assertEquals(response.getCode(), 200); } @@ -349,6 +478,13 @@ assertEquals(response.getCode(), 200); checkValuePB(TABLE, ROW_1, COLUMN_1, VALUE_2); + response = checkAndPutValuePB(TABLE, ROW_1, COLUMN_1, VALUE_2, VALUE_3); + assertEquals(response.getCode(), 200); + checkValuePB(TABLE, ROW_1, COLUMN_1, VALUE_3); + response = checkAndPutValueXML(TABLE, ROW_1, COLUMN_1, VALUE_3, VALUE_4); + assertEquals(response.getCode(), 200); + checkValuePB(TABLE, ROW_1, COLUMN_1, VALUE_4); + response = deleteRow(TABLE, ROW_1); assertEquals(response.getCode(), 200); } @@ -365,7 +501,7 @@ assertEquals(response.getCode(), 200); assertTrue(Bytes.equals(response.getBody(), body)); boolean foundTimestampHeader = false; - for (Header header: response.getHeaders()) { + for (Header header : response.getHeaders()) { if (header.getName().equals("X-Timestamp")) { foundTimestampHeader = true; break; @@ -381,7 +517,7 @@ public void testSingleCellGetJSON() throws IOException, JAXBException { final String path = "/" + TABLE + "/" + ROW_4 + "/" + COLUMN_1; Response response = client.put(path, Constants.MIMETYPE_BINARY, - Bytes.toBytes(VALUE_4)); + Bytes.toBytes(VALUE_4)); assertEquals(response.getCode(), 200); Thread.yield(); response = client.get(path, Constants.MIMETYPE_JSON); @@ -401,48 +537,44 @@ path.append('/'); path.append(COLUMN_1); Response response; - response = putValueXML(path.toString(), TABLE, urlKey, COLUMN_1, - VALUE_1); + response = putValueXML(path.toString(), TABLE, urlKey, COLUMN_1, VALUE_1); assertEquals(response.getCode(), 200); checkValueXML(path.toString(), TABLE, urlKey, COLUMN_1, VALUE_1); } @Test public void testNoSuchCF() throws IOException, JAXBException { - final String goodPath = "/" + TABLE + "/" + ROW_1 + "/" + CFA+":"; + final String goodPath = "/" + TABLE + "/" + ROW_1 + "/" + CFA + ":"; final String badPath = "/" + TABLE + "/" + ROW_1 + "/" + "BAD"; Response response = client.post(goodPath, Constants.MIMETYPE_BINARY, - Bytes.toBytes(VALUE_1)); + Bytes.toBytes(VALUE_1)); assertEquals(response.getCode(), 200); - assertEquals(client.get(goodPath, Constants.MIMETYPE_BINARY).getCode(), - 200); - assertEquals(client.get(badPath, Constants.MIMETYPE_BINARY).getCode(), - 404); - assertEquals(client.get(goodPath, Constants.MIMETYPE_BINARY).getCode(), - 200); + assertEquals(client.get(goodPath, Constants.MIMETYPE_BINARY).getCode(), 200); + assertEquals(client.get(badPath, Constants.MIMETYPE_BINARY).getCode(), 404); + assertEquals(client.get(goodPath, Constants.MIMETYPE_BINARY).getCode(), 200); } @Test public void testMultiCellGetPutXML() throws IOException, JAXBException { - String path = "/" + TABLE + "/fakerow"; // deliberate nonexistent row + String path = "/" + TABLE + "/fakerow"; // deliberate nonexistent row CellSetModel cellSetModel = new CellSetModel(); RowModel rowModel = new RowModel(ROW_1); - rowModel.addCell(new CellModel(Bytes.toBytes(COLUMN_1), - Bytes.toBytes(VALUE_1))); - rowModel.addCell(new CellModel(Bytes.toBytes(COLUMN_2), - Bytes.toBytes(VALUE_2))); + rowModel.addCell(new CellModel(Bytes.toBytes(COLUMN_1), Bytes + .toBytes(VALUE_1))); + rowModel.addCell(new CellModel(Bytes.toBytes(COLUMN_2), Bytes + .toBytes(VALUE_2))); cellSetModel.addRow(rowModel); rowModel = new RowModel(ROW_2); - rowModel.addCell(new CellModel(Bytes.toBytes(COLUMN_1), - Bytes.toBytes(VALUE_3))); - rowModel.addCell(new CellModel(Bytes.toBytes(COLUMN_2), - Bytes.toBytes(VALUE_4))); + rowModel.addCell(new CellModel(Bytes.toBytes(COLUMN_1), Bytes + .toBytes(VALUE_3))); + rowModel.addCell(new CellModel(Bytes.toBytes(COLUMN_2), Bytes + .toBytes(VALUE_4))); cellSetModel.addRow(rowModel); StringWriter writer = new StringWriter(); marshaller.marshal(cellSetModel, writer); Response response = client.put(path, Constants.MIMETYPE_XML, - Bytes.toBytes(writer.toString())); + Bytes.toBytes(writer.toString())); Thread.yield(); // make sure the fake row was not actually created @@ -463,23 +595,23 @@ @Test public void testMultiCellGetPutPB() throws IOException { - String path = "/" + TABLE + "/fakerow"; // deliberate nonexistent row + String path = "/" + TABLE + "/fakerow"; // deliberate nonexistent row CellSetModel cellSetModel = new CellSetModel(); RowModel rowModel = new RowModel(ROW_1); - rowModel.addCell(new CellModel(Bytes.toBytes(COLUMN_1), - Bytes.toBytes(VALUE_1))); - rowModel.addCell(new CellModel(Bytes.toBytes(COLUMN_2), - Bytes.toBytes(VALUE_2))); + rowModel.addCell(new CellModel(Bytes.toBytes(COLUMN_1), Bytes + .toBytes(VALUE_1))); + rowModel.addCell(new CellModel(Bytes.toBytes(COLUMN_2), Bytes + .toBytes(VALUE_2))); cellSetModel.addRow(rowModel); rowModel = new RowModel(ROW_2); - rowModel.addCell(new CellModel(Bytes.toBytes(COLUMN_1), - Bytes.toBytes(VALUE_3))); - rowModel.addCell(new CellModel(Bytes.toBytes(COLUMN_2), - Bytes.toBytes(VALUE_4))); + rowModel.addCell(new CellModel(Bytes.toBytes(COLUMN_1), Bytes + .toBytes(VALUE_3))); + rowModel.addCell(new CellModel(Bytes.toBytes(COLUMN_2), Bytes + .toBytes(VALUE_4))); cellSetModel.addRow(rowModel); Response response = client.put(path, Constants.MIMETYPE_PROTOBUF, - cellSetModel.createProtobufOutput()); + cellSetModel.createProtobufOutput()); Thread.yield(); // make sure the fake row was not actually created @@ -501,7 +633,7 @@ @Test public void testStartEndRowGetPutXML() throws IOException, JAXBException { String[] rows = { ROW_1, ROW_2, ROW_3 }; - String[] values = { VALUE_1, VALUE_2, VALUE_3 }; + String[] values = { VALUE_1, VALUE_2, VALUE_3 }; Response response = null; for (int i = 0; i < rows.length; i++) { response = putValueXML(TABLE, rows[i], COLUMN_1, values[i]); @@ -510,15 +642,15 @@ } response = getValueXML(TABLE, rows[0], rows[2], COLUMN_1); assertEquals(200, response.getCode()); - CellSetModel cellSet = (CellSetModel) - unmarshaller.unmarshal(new ByteArrayInputStream(response.getBody())); + CellSetModel cellSet = (CellSetModel) unmarshaller + .unmarshal(new ByteArrayInputStream(response.getBody())); assertEquals(2, cellSet.getRows().size()); - for (int i = 0; i < cellSet.getRows().size()-1; i++) { + for (int i = 0; i < cellSet.getRows().size() - 1; i++) { RowModel rowModel = cellSet.getRows().get(i); - for (CellModel cell: rowModel.getCells()) { + for (CellModel cell : rowModel.getCells()) { assertEquals(COLUMN_1, Bytes.toString(cell.getColumn())); assertEquals(values[i], Bytes.toString(cell.getValue())); - } + } } for (String row : rows) { response = deleteRow(TABLE, row); Index: src/test/java/org/apache/hadoop/hbase/rest/client/TestRemoteTable.java =================================================================== --- src/test/java/org/apache/hadoop/hbase/rest/client/TestRemoteTable.java (revision 1215035) +++ src/test/java/org/apache/hadoop/hbase/rest/client/TestRemoteTable.java (working copy) @@ -69,8 +69,7 @@ private static final long TS_1 = TS_2 - ONE_HOUR; private static final HBaseTestingUtility TEST_UTIL = new HBaseTestingUtility(); - private static final HBaseRESTTestingUtility REST_TEST_UTIL = - new HBaseRESTTestingUtility(); + private static final HBaseRESTTestingUtility REST_TEST_UTIL = new HBaseRESTTestingUtility(); private static RemoteHTable remoteTable; @BeforeClass @@ -78,8 +77,8 @@ TEST_UTIL.startMiniCluster(3); REST_TEST_UTIL.startServletContainer(TEST_UTIL.getConfiguration()); HBaseAdmin admin = TEST_UTIL.getHBaseAdmin(); - LOG.info("Admin Connection=" + admin.getConnection() + ", " + - admin.getConnection().getZooKeeperWatcher()); + LOG.info("Admin Connection=" + admin.getConnection() + ", " + + admin.getConnection().getZooKeeperWatcher()); if (!admin.tableExists(TABLE)) { HTableDescriptor htd = new HTableDescriptor(TABLE); htd.addFamily(new HColumnDescriptor(COLUMN_1)); @@ -87,8 +86,8 @@ htd.addFamily(new HColumnDescriptor(COLUMN_3)); admin.createTable(htd); HTable table = new HTable(TEST_UTIL.getConfiguration(), TABLE); - LOG.info("Table connection=" + table.getConnection() + ", " + - admin.getConnection().getZooKeeperWatcher()); + LOG.info("Table connection=" + table.getConnection() + ", " + + admin.getConnection().getZooKeeperWatcher()); Put put = new Put(ROW_1); put.add(COLUMN_1, QUALIFIER_1, TS_2, VALUE_1); table.put(put); @@ -99,10 +98,9 @@ table.put(put); table.flushCommits(); } - remoteTable = new RemoteHTable( - new Client(new Cluster().add("localhost", - REST_TEST_UTIL.getServletPort())), - TEST_UTIL.getConfiguration(), TABLE, null); + remoteTable = new RemoteHTable(new Client(new Cluster().add("localhost", + REST_TEST_UTIL.getServletPort())), TEST_UTIL.getConfiguration(), TABLE, + null); } @AfterClass @@ -114,8 +112,8 @@ @Test public void testGetTableDescriptor() throws IOException { - HTableDescriptor local = new HTable(TEST_UTIL.getConfiguration(), - TABLE).getTableDescriptor(); + HTableDescriptor local = new HTable(TEST_UTIL.getConfiguration(), TABLE) + .getTableDescriptor(); assertEquals(remoteTable.getTableDescriptor(), local); } @@ -148,7 +146,7 @@ assertNull(value2); get = new Get(ROW_2); - result = remoteTable.get(get); + result = remoteTable.get(get); value1 = result.getValue(COLUMN_1, QUALIFIER_1); value2 = result.getValue(COLUMN_2, QUALIFIER_2); assertNotNull(value1); @@ -158,7 +156,7 @@ get = new Get(ROW_2); get.addFamily(COLUMN_1); - result = remoteTable.get(get); + result = remoteTable.get(get); value1 = result.getValue(COLUMN_1, QUALIFIER_1); value2 = result.getValue(COLUMN_2, QUALIFIER_2); assertNotNull(value1); @@ -168,7 +166,7 @@ get = new Get(ROW_2); get.addColumn(COLUMN_1, QUALIFIER_1); get.addColumn(COLUMN_2, QUALIFIER_2); - result = remoteTable.get(get); + result = remoteTable.get(get); value1 = result.getValue(COLUMN_1, QUALIFIER_1); value2 = result.getValue(COLUMN_2, QUALIFIER_2); assertNotNull(value1); @@ -182,7 +180,7 @@ get.addFamily(COLUMN_1); get.addFamily(COLUMN_2); get.setTimeStamp(TS_1); - result = remoteTable.get(get); + result = remoteTable.get(get); value1 = result.getValue(COLUMN_1, QUALIFIER_1); value2 = result.getValue(COLUMN_2, QUALIFIER_2); assertNotNull(value1); @@ -195,7 +193,7 @@ get.addFamily(COLUMN_1); get.addFamily(COLUMN_2); get.setTimeRange(0, TS_1 + 1); - result = remoteTable.get(get); + result = remoteTable.get(get); value1 = result.getValue(COLUMN_1, QUALIFIER_1); value2 = result.getValue(COLUMN_2, QUALIFIER_2); assertNotNull(value1); @@ -209,7 +207,7 @@ get.setMaxVersions(2); result = remoteTable.get(get); int count = 0; - for (KeyValue kv: result.list()) { + for (KeyValue kv : result.list()) { if (Bytes.equals(COLUMN_1, kv.getFamily()) && TS_1 == kv.getTimestamp()) { assertTrue(Bytes.equals(VALUE_1, kv.getValue())); // @TS_1 count++; @@ -265,6 +263,24 @@ assertTrue(Bytes.equals(VALUE_2, value)); } + @Test + public void testCheckAndPut() throws IOException { + Put put = new Put(ROW_3); + put.add(COLUMN_1, QUALIFIER_1, VALUE_1); + remoteTable.put(put); + + Put checkAndPut = new Put(ROW_3); + checkAndPut.add(COLUMN_1, QUALIFIER_1, VALUE_2); + remoteTable.checkAndPut(ROW_3, COLUMN_1, QUALIFIER_1, VALUE_1, checkAndPut); + + Get get = new Get(ROW_3); + get.addFamily(COLUMN_1); + Result result = remoteTable.get(get); + byte[] value = result.getValue(COLUMN_1, QUALIFIER_1); + assertNotNull(value); + assertTrue(Bytes.equals(VALUE_2, value)); + } + public void testDelete() throws IOException { Put put = new Put(ROW_3); put.add(COLUMN_1, QUALIFIER_1, VALUE_1); @@ -285,7 +301,7 @@ Delete delete = new Delete(ROW_3); delete.deleteColumn(COLUMN_2, QUALIFIER_2); remoteTable.delete(delete); - + get = new Get(ROW_3); get.addFamily(COLUMN_1); get.addFamily(COLUMN_2); @@ -309,6 +325,51 @@ assertNull(value2); } + public void testCheckAndDelete() throws IOException { + Put put = new Put(ROW_3); + put.add(COLUMN_1, QUALIFIER_1, VALUE_1); + put.add(COLUMN_2, QUALIFIER_2, VALUE_2); + remoteTable.put(put); + + Get get = new Get(ROW_3); + get.addFamily(COLUMN_1); + get.addFamily(COLUMN_2); + Result result = remoteTable.get(get); + byte[] value1 = result.getValue(COLUMN_1, QUALIFIER_1); + byte[] value2 = result.getValue(COLUMN_2, QUALIFIER_2); + assertNotNull(value1); + assertTrue(Bytes.equals(VALUE_1, value1)); + assertNotNull(value2); + assertTrue(Bytes.equals(VALUE_2, value2)); + + Delete delete = new Delete(ROW_3); + delete.deleteColumn(COLUMN_2, QUALIFIER_2); + remoteTable.checkAndDelete(ROW_3, COLUMN_2, QUALIFIER_2, VALUE_2, delete); + + get = new Get(ROW_3); + get.addFamily(COLUMN_1); + get.addFamily(COLUMN_2); + result = remoteTable.get(get); + value1 = result.getValue(COLUMN_1, QUALIFIER_1); + value2 = result.getValue(COLUMN_2, QUALIFIER_2); + assertNotNull(value1); + assertTrue(Bytes.equals(VALUE_1, value1)); + assertNull(value2); + + delete = new Delete(ROW_3); + delete.deleteColumn(COLUMN_1, QUALIFIER_1); + remoteTable.checkAndDelete(ROW_3, COLUMN_1, QUALIFIER_1, VALUE_1, delete); + + get = new Get(ROW_3); + get.addFamily(COLUMN_1); + get.addFamily(COLUMN_2); + result = remoteTable.get(get); + value1 = result.getValue(COLUMN_1, QUALIFIER_1); + value2 = result.getValue(COLUMN_2, QUALIFIER_2); + assertNull(value1); + assertNull(value2); + } + public void testScanner() throws IOException { List puts = new ArrayList(); Put put = new Put(ROW_1); Index: src/main/java/org/apache/hadoop/hbase/rest/TableResource.java =================================================================== --- src/main/java/org/apache/hadoop/hbase/rest/TableResource.java (revision 1215035) +++ src/main/java/org/apache/hadoop/hbase/rest/TableResource.java (working copy) @@ -21,218 +21,24 @@ package org.apache.hadoop.hbase.rest; import java.io.IOException; -import java.util.Map; -import java.util.concurrent.ConcurrentHashMap; -import java.util.concurrent.ConcurrentSkipListMap; -import java.util.regex.Matcher; -import java.util.regex.Pattern; import javax.ws.rs.Encoded; -import javax.ws.rs.Path; -import javax.ws.rs.PathParam; import javax.ws.rs.QueryParam; +import javax.ws.rs.PathParam; +import javax.ws.rs.Path; -import org.apache.hadoop.hbase.HColumnDescriptor; -import org.apache.hadoop.hbase.HConstants; -import org.apache.hadoop.hbase.HTableDescriptor; -import org.apache.hadoop.hbase.client.HTableInterface; -import org.apache.hadoop.hbase.client.HTablePool; -import org.apache.hadoop.hbase.io.ImmutableBytesWritable; -import org.apache.hadoop.hbase.rest.transform.NullTransform; -import org.apache.hadoop.hbase.rest.transform.Transform; -import org.apache.hadoop.hbase.util.Bytes; - public class TableResource extends ResourceBase { /** - * HCD attributes starting with this string are considered transform - * directives - */ - private static final String DIRECTIVE_KEY = "Transform$"; - - /** - * Transform directives are of the form <qualifier>:<class> - * where qualifier is a string for exact matching or '*' as a wildcard - * that will match anything; and class is either the fully qualified - * class name of a transform implementation or can be the short name of a - * transform in the org.apache.hadoop.hbase.rest.transform package. - */ - private static final Pattern DIRECTIVE_PATTERN = - Pattern.compile("([^\\:]+)\\:([^\\,]+)\\,?"); - private static final Transform defaultTransform = new NullTransform(); - private static final - Map>> transformMap = - new ConcurrentHashMap>>(); - private static final Map lastCheckedMap = - new ConcurrentHashMap(); - - /** - * @param table the table - * @param family the column family - * @param qualifier the column qualifier, or null - * @return the transformation specified for the given family or qualifier, if - * any, otherwise the default - */ - static Transform getTransform(String table, byte[] family, byte[] qualifier) { - if (qualifier == null) { - qualifier = HConstants.EMPTY_BYTE_ARRAY; - } - Map> familyMap = transformMap.get(table); - if (familyMap != null) { - Map columnMap = familyMap.get(family); - if (columnMap != null) { - Transform t = columnMap.get(qualifier); - // check as necessary if there is a wildcard entry - if (t == null) { - t = columnMap.get(HConstants.EMPTY_BYTE_ARRAY); - } - // if we found something, return it, otherwise we will return the - // default by falling through - if (t != null) { - return t; - } - } - } - return defaultTransform; - } - - synchronized static void setTransform(String table, byte[] family, - byte[] qualifier, Transform transform) { - Map> familyMap = transformMap.get(table); - if (familyMap == null) { - familyMap = new ConcurrentSkipListMap>( - Bytes.BYTES_COMPARATOR); - transformMap.put(table, familyMap); - } - Map columnMap = familyMap.get(family); - if (columnMap == null) { - columnMap = new ConcurrentSkipListMap( - Bytes.BYTES_COMPARATOR); - familyMap.put(family, columnMap); - } - // if transform is null, remove any existing entry - if (transform != null) { - columnMap.put(qualifier, transform); - } else { - columnMap.remove(qualifier); - } - } - - String table; - - /** - * Scan the table schema for transform directives. These are column family - * attributes containing a comma-separated list of elements of the form - * <qualifier>:<transform-class>, where qualifier - * can be a string for exact matching or '*' as a wildcard to match anything. - * The attribute key must begin with the string "Transform$". - */ - void scanTransformAttrs() throws IOException { - HTableDescriptor htd = null; - try { - HTablePool pool = servlet.getTablePool(); - HTableInterface t = pool.getTable(table); - try { - htd = t.getTableDescriptor(); - } finally { - pool.putTable(t); - } - } catch (Exception e) { - // HTablePool#getTable throws RTE, and it doesn't really matter what - // exception got us here anyway, htd will be null below - } - if (htd == null) { - return; - } - for (HColumnDescriptor hcd: htd.getFamilies()) { - for (Map.Entry e: - hcd.getValues().entrySet()) { - // does the key start with the transform directive tag? - String key = Bytes.toString(e.getKey().get()); - if (!key.startsWith(DIRECTIVE_KEY)) { - // no, skip - continue; - } - // match a comma separated list of one or more directives - byte[] value = e.getValue().get(); - Matcher m = DIRECTIVE_PATTERN.matcher(Bytes.toString(value)); - while (m.find()) { - byte[] qualifier = HConstants.EMPTY_BYTE_ARRAY; - String s = m.group(1); - if (s.length() > 0 && !s.equals("*")) { - qualifier = Bytes.toBytes(s); - } - String className = m.group(2); - try { - // if a transform was previously configured for the qualifier, - // this will simply replace it - setTransform(table, hcd.getName(), qualifier, - (Transform)Class.forName(className).newInstance()); - } catch (ClassNotFoundException ex) { - className = "org.apache.hadoop.hbase.rest.transform." + className; - try { - setTransform(table, hcd.getName(), qualifier, - (Transform)Class.forName(className).newInstance()); - } catch (Exception ex2) { - throw new IOException("Cannot instantiate transform", ex2); - } - } catch (Exception ex) { - throw new IOException("Cannot instantiate transform", ex); - } - } - } - } - } - - /** * Constructor + * * @param table * @throws IOException */ public TableResource(String table) throws IOException { - super(); - this.table = table; - // Scanning the table schema is too expensive to do for every operation. - // Do it once per minute by default. - // Setting hbase.rest.transform.check.interval to <= 0 disables rescanning. - long now = System.currentTimeMillis(); - Long lastChecked = lastCheckedMap.get(table); - if (lastChecked != null) { - long interval = servlet.getConfiguration() - .getLong("hbase.rest.transform.check.interval", 60000); - if (interval > 0 && (now - lastChecked.longValue()) > interval) { - scanTransformAttrs(); - lastCheckedMap.put(table, now); - } - } else { - scanTransformAttrs(); - lastCheckedMap.put(table, now); - } + super(table); } - /** @return the table name */ - String getName() { - return table; - } - - /** - * Apply any configured transformations to the value - * @param family - * @param qualifier - * @param value - * @param direction - * @return - * @throws IOException - */ - byte[] transform(byte[] family, byte[] qualifier, byte[] value, - Transform.Direction direction) throws IOException { - Transform t = getTransform(table, family, qualifier); - if (t != null) { - return t.transform(value, direction); - } - return value; - } - @Path("exists") public ExistsResource getExistsResource() throws IOException { return new ExistsResource(this); Index: src/main/java/org/apache/hadoop/hbase/rest/CheckAndPutTableResource.java =================================================================== --- src/main/java/org/apache/hadoop/hbase/rest/CheckAndPutTableResource.java (revision 0) +++ src/main/java/org/apache/hadoop/hbase/rest/CheckAndPutTableResource.java (revision 0) @@ -0,0 +1,50 @@ +/* + * Copyright The Apache Software Foundation + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.hadoop.hbase.rest; + +import java.io.IOException; + +import javax.ws.rs.QueryParam; +import javax.ws.rs.PathParam; +import javax.ws.rs.Path; +import javax.ws.rs.Encoded; + +public class CheckAndPutTableResource extends ResourceBase { + + /** + * Constructor + * + * @param table + * @throws IOException + */ + public CheckAndPutTableResource(String table) throws IOException { + super(table); + } + + @Path("{rowspec: .+}") + public CheckAndPutRowResource getCheckAndPutRowResource( + // 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) throws IOException { + return new CheckAndPutRowResource(this, rowspec, versions); + } +} Index: src/main/java/org/apache/hadoop/hbase/rest/CheckAndPutRowResource.java =================================================================== --- src/main/java/org/apache/hadoop/hbase/rest/CheckAndPutRowResource.java (revision 0) +++ src/main/java/org/apache/hadoop/hbase/rest/CheckAndPutRowResource.java (revision 0) @@ -0,0 +1,165 @@ +/* + * Copyright 2010 The Apache Software Foundation + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.hadoop.hbase.rest; + +import java.io.IOException; + +import javax.ws.rs.Consumes; +import javax.ws.rs.POST; +import javax.ws.rs.PUT; +import javax.ws.rs.WebApplicationException; +import javax.ws.rs.core.Context; +import javax.ws.rs.core.Response; +import javax.ws.rs.core.UriInfo; +import javax.ws.rs.core.Response.ResponseBuilder; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import org.apache.hadoop.hbase.KeyValue; +import org.apache.hadoop.hbase.client.HTable; +import org.apache.hadoop.hbase.client.HTableInterface; +import org.apache.hadoop.hbase.client.HTablePool; +import org.apache.hadoop.hbase.client.Put; +import org.apache.hadoop.hbase.rest.model.CellModel; +import org.apache.hadoop.hbase.rest.model.CellSetModel; +import org.apache.hadoop.hbase.rest.model.RowModel; +import org.apache.hadoop.hbase.rest.transform.Transform; + +public class CheckAndPutRowResource extends ResourceBase { + private static final Log LOG = LogFactory + .getLog(CheckAndPutRowResource.class); + + CheckAndPutTableResource tableResource; + RowSpec rowspec; + + /** + * Constructor + * + * @param tableResource + * @param rowspec + * @param versions + * @throws IOException + */ + public CheckAndPutRowResource(CheckAndPutTableResource tableResource, + String rowspec, String versions) throws IOException { + super(); + this.tableResource = tableResource; + this.rowspec = new RowSpec(rowspec); + if (versions != null) { + this.rowspec.setMaxVersions(Integer.valueOf(versions)); + } + } + + Response update(final CellSetModel model, final boolean replace) { + servlet.getMetrics().incrementRequests(1); + if (servlet.isReadOnly()) { + throw new WebApplicationException(Response.Status.FORBIDDEN); + } + HTablePool pool = servlet.getTablePool(); + HTableInterface table = null; + try { + RowModel rowModel = model.getRows().get(0); + byte[] key = rowModel.getKey(); + if (key == null) { + key = rowspec.getRow(); + } + if (key == null) { + throw new WebApplicationException(Response.Status.BAD_REQUEST); + } + + if (rowModel.getCells().size() < 2) { + throw new WebApplicationException(Response.Status.BAD_REQUEST); + } + + Put put = new Put(key); + + CellModel valueToPutCell = rowModel.getCells().get(0); + byte[] valueToPutColumn = valueToPutCell.getColumn(); + if (valueToPutColumn == null) { + try { + valueToPutColumn = rowspec.getColumns()[0]; + } catch (final ArrayIndexOutOfBoundsException e) { + throw new WebApplicationException(Response.Status.BAD_REQUEST); + } + } + + byte[][] valueToPutParts = KeyValue.parseColumn(valueToPutColumn); + if (valueToPutParts.length == 2 && valueToPutParts[1].length > 0) { + put.add(valueToPutParts[0], valueToPutParts[1], valueToPutCell + .getTimestamp(), tableResource.transform(valueToPutParts[0], + valueToPutParts[1], valueToPutCell.getValue(), + Transform.Direction.IN)); + } else { + throw new WebApplicationException(Response.Status.BAD_REQUEST); + } + + CellModel valueToCheckCell = rowModel.getCells().get(1); + byte[] valueToCheckColumn = valueToCheckCell.getColumn(); + if (valueToCheckColumn == null) { + try { + valueToCheckColumn = rowspec.getColumns()[1]; + } catch (final ArrayIndexOutOfBoundsException e) { + throw new WebApplicationException(Response.Status.BAD_REQUEST); + } + } + + table = pool.getTable(tableResource.getName()); + ((HTable) table).setAutoFlush(false); + boolean retValue = table.checkAndPut(key, valueToPutParts[0], + valueToPutParts[1], valueToCheckCell.getValue(), put); + if (LOG.isDebugEnabled()) { + LOG.debug("CHECK-AND-PUT " + put.toString() + ", returns " + retValue); + } + ((HTable) table).setAutoFlush(true); + table.flushCommits(); + ResponseBuilder response = Response.ok(); + if (!retValue) { + response = Response.status(304); + } + return response.build(); + } catch (final IOException e) { + throw new WebApplicationException(e, Response.Status.SERVICE_UNAVAILABLE); + } finally { + if (table != null) { + pool.putTable(table); + } + } + } + + @PUT + @Consumes({ MIMETYPE_XML, MIMETYPE_JSON, MIMETYPE_PROTOBUF }) + public Response put(final CellSetModel model, final @Context UriInfo uriInfo) { + if (LOG.isDebugEnabled()) { + LOG.debug("PUT " + uriInfo.getAbsolutePath()); + } + return update(model, true); + } + + @POST + @Consumes({ MIMETYPE_XML, MIMETYPE_JSON, MIMETYPE_PROTOBUF }) + public Response post(final CellSetModel model, final @Context UriInfo uriInfo) { + if (LOG.isDebugEnabled()) { + LOG.debug("POST " + uriInfo.getAbsolutePath()); + } + return update(model, false); + } +} Index: src/main/java/org/apache/hadoop/hbase/rest/CheckAndDeleteTableResource.java =================================================================== --- src/main/java/org/apache/hadoop/hbase/rest/CheckAndDeleteTableResource.java (revision 0) +++ src/main/java/org/apache/hadoop/hbase/rest/CheckAndDeleteTableResource.java (revision 0) @@ -0,0 +1,50 @@ +/* + * Copyright The Apache Software Foundation + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.hadoop.hbase.rest; + +import java.io.IOException; + +import javax.ws.rs.QueryParam; +import javax.ws.rs.PathParam; +import javax.ws.rs.Path; +import javax.ws.rs.Encoded; + +public class CheckAndDeleteTableResource extends ResourceBase { + + /** + * Constructor + * + * @param table + * @throws IOException + */ + public CheckAndDeleteTableResource(String table) throws IOException { + super(table); + } + + @Path("{rowspec: .+}") + public CheckAndDeleteRowResource getCheckAndDeleteRowResource( + // 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) throws IOException { + return new CheckAndDeleteRowResource(this, rowspec, versions); + } +} Index: src/main/java/org/apache/hadoop/hbase/rest/ResourceBase.java =================================================================== --- src/main/java/org/apache/hadoop/hbase/rest/ResourceBase.java (revision 1215035) +++ src/main/java/org/apache/hadoop/hbase/rest/ResourceBase.java (working copy) @@ -21,12 +21,213 @@ package org.apache.hadoop.hbase.rest; import java.io.IOException; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentSkipListMap; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import org.apache.hadoop.hbase.HColumnDescriptor; +import org.apache.hadoop.hbase.HConstants; +import org.apache.hadoop.hbase.HTableDescriptor; +import org.apache.hadoop.hbase.client.HTableInterface; +import org.apache.hadoop.hbase.client.HTablePool; +import org.apache.hadoop.hbase.io.ImmutableBytesWritable; +import org.apache.hadoop.hbase.rest.transform.NullTransform; +import org.apache.hadoop.hbase.rest.transform.Transform; +import org.apache.hadoop.hbase.util.Bytes; + public class ResourceBase implements Constants { RESTServlet servlet; + protected String table; + /** + * HCD attributes starting with this string are considered transform + * directives + */ + protected static final String DIRECTIVE_KEY = "Transform$"; + + /** + * Transform directives are of the form + * <qualifier>:<class> where qualifier is a + * string for exact matching or '*' as a wildcard that will match anything; + * and class is either the fully qualified class name of a transform + * implementation or can be the short name of a transform in the + * org.apache.hadoop.hbase.rest.transform package. + */ + protected static final Pattern DIRECTIVE_PATTERN = Pattern + .compile("([^\\:]+)\\:([^\\,]+)\\,?"); + protected static final Transform defaultTransform = new NullTransform(); + protected static final Map>> transformMap = new ConcurrentHashMap>>(); + protected static final Map lastCheckedMap = new ConcurrentHashMap(); + public ResourceBase() throws IOException { servlet = RESTServlet.getInstance(); } + + public ResourceBase(String table) throws IOException { + servlet = RESTServlet.getInstance(); + this.table = table; + // Scanning the table schema is too expensive to do for every operation. + // Do it once per minute by default. + // Setting hbase.rest.transform.check.interval to <= 0 disables rescanning. + long now = System.currentTimeMillis(); + Long lastChecked = lastCheckedMap.get(table); + if (lastChecked != null) { + long interval = servlet.getConfiguration().getLong( + "hbase.rest.transform.check.interval", 60000); + if (interval > 0 && (now - lastChecked.longValue()) > interval) { + scanTransformAttrs(); + lastCheckedMap.put(table, now); + } + } else { + scanTransformAttrs(); + lastCheckedMap.put(table, now); + } + } + + /** @return the table name */ + protected String getName() { + return table; + } + + /** + * @param table + * the table + * @param family + * the column family + * @param qualifier + * the column qualifier, or null + * @return the transformation specified for the given family or qualifier, if + * any, otherwise the default + */ + protected static Transform getTransform(String table, byte[] family, + byte[] qualifier) { + if (qualifier == null) { + qualifier = HConstants.EMPTY_BYTE_ARRAY; + } + Map> familyMap = transformMap.get(table); + if (familyMap != null) { + Map columnMap = familyMap.get(family); + if (columnMap != null) { + Transform t = columnMap.get(qualifier); + // check as necessary if there is a wildcard entry + if (t == null) { + t = columnMap.get(HConstants.EMPTY_BYTE_ARRAY); + } + // if we found something, return it, otherwise we will return the + // default by falling through + if (t != null) { + return t; + } + } + } + return defaultTransform; + } + + protected synchronized static void setTransform(String table, byte[] family, + byte[] qualifier, Transform transform) { + Map> familyMap = transformMap.get(table); + if (familyMap == null) { + familyMap = new ConcurrentSkipListMap>( + Bytes.BYTES_COMPARATOR); + transformMap.put(table, familyMap); + } + Map columnMap = familyMap.get(family); + if (columnMap == null) { + columnMap = new ConcurrentSkipListMap( + Bytes.BYTES_COMPARATOR); + familyMap.put(family, columnMap); + } + // if transform is null, remove any existing entry + if (transform != null) { + columnMap.put(qualifier, transform); + } else { + columnMap.remove(qualifier); + } + } + + /** + * Scan the table schema for transform directives. These are column family + * attributes containing a comma-separated list of elements of the form + * <qualifier>:<transform-class>, where qualifier can be + * a string for exact matching or '*' as a wildcard to match anything. The + * attribute key must begin with the string "Transform$". + */ + void scanTransformAttrs() throws IOException { + HTableDescriptor htd = null; + try { + HTablePool pool = servlet.getTablePool(); + HTableInterface t = pool.getTable(table); + try { + htd = t.getTableDescriptor(); + } finally { + pool.putTable(t); + } + } catch (Exception e) { + // HTablePool#getTable throws RTE, and it doesn't really matter what + // exception got us here anyway, htd will be null below + } + if (htd == null) { + return; + } + for (HColumnDescriptor hcd : htd.getFamilies()) { + for (Map.Entry e : hcd + .getValues().entrySet()) { + // does the key start with the transform directive tag? + String key = Bytes.toString(e.getKey().get()); + if (!key.startsWith(DIRECTIVE_KEY)) { + // no, skip + continue; + } + // match a comma separated list of one or more directives + byte[] value = e.getValue().get(); + Matcher m = DIRECTIVE_PATTERN.matcher(Bytes.toString(value)); + while (m.find()) { + byte[] qualifier = HConstants.EMPTY_BYTE_ARRAY; + String s = m.group(1); + if (s.length() > 0 && !s.equals("*")) { + qualifier = Bytes.toBytes(s); + } + String className = m.group(2); + try { + // if a transform was previously configured for the qualifier, + // this will simply replace it + setTransform(table, hcd.getName(), qualifier, (Transform) Class + .forName(className).newInstance()); + } catch (ClassNotFoundException ex) { + className = "org.apache.hadoop.hbase.rest.transform." + className; + try { + setTransform(table, hcd.getName(), qualifier, (Transform) Class + .forName(className).newInstance()); + } catch (Exception ex2) { + throw new IOException("Cannot instantiate transform", ex2); + } + } catch (Exception ex) { + throw new IOException("Cannot instantiate transform", ex); + } + } + } + } + } + + /** + * Apply any configured transformations to the value + * + * @param family + * @param qualifier + * @param value + * @param direction + * @return + * @throws IOException + */ + protected byte[] transform(byte[] family, byte[] qualifier, byte[] value, + Transform.Direction direction) throws IOException { + Transform t = getTransform(table, family, qualifier); + if (t != null) { + return t.transform(value, direction); + } + return value; + } } Index: src/main/java/org/apache/hadoop/hbase/rest/CheckAndDeleteRowResource.java =================================================================== --- src/main/java/org/apache/hadoop/hbase/rest/CheckAndDeleteRowResource.java (revision 0) +++ src/main/java/org/apache/hadoop/hbase/rest/CheckAndDeleteRowResource.java (revision 0) @@ -0,0 +1,150 @@ +/* + * Copyright The Apache Software Foundation + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.hadoop.hbase.rest; + +import java.io.IOException; + +import org.apache.hadoop.hbase.KeyValue; +import org.apache.hadoop.hbase.client.Delete; +import org.apache.hadoop.hbase.client.HTable; +import org.apache.hadoop.hbase.client.HTableInterface; +import org.apache.hadoop.hbase.client.HTablePool; +import org.apache.hadoop.hbase.rest.model.CellModel; +import org.apache.hadoop.hbase.rest.model.CellSetModel; +import org.apache.hadoop.hbase.rest.model.RowModel; + +import javax.ws.rs.Consumes; +import javax.ws.rs.POST; +import javax.ws.rs.PUT; +import javax.ws.rs.WebApplicationException; +import javax.ws.rs.core.Context; +import javax.ws.rs.core.Response; +import javax.ws.rs.core.UriInfo; +import javax.ws.rs.core.Response.ResponseBuilder; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +public class CheckAndDeleteRowResource extends ResourceBase { + private static final Log LOG = LogFactory + .getLog(CheckAndDeleteRowResource.class); + + CheckAndDeleteTableResource tableResource; + RowSpec rowspec; + + /** + * Constructor + * + * @param tableResource + * @param rowspec + * @param versions + * @throws IOException + */ + public CheckAndDeleteRowResource(CheckAndDeleteTableResource tableResource, + String rowspec, String versions) throws IOException { + super(); + this.tableResource = tableResource; + this.rowspec = new RowSpec(rowspec); + if (versions != null) { + this.rowspec.setMaxVersions(Integer.valueOf(versions)); + } + } + + Response update(final CellSetModel model, final boolean replace) { + servlet.getMetrics().incrementRequests(1); + if (servlet.isReadOnly()) { + throw new WebApplicationException(Response.Status.FORBIDDEN); + } + HTablePool pool = servlet.getTablePool(); + HTableInterface table = null; + Delete delete = null; + try { + RowModel rowModel = model.getRows().get(0); + byte[] key = rowModel.getKey(); + if (key == null) { + key = rowspec.getRow(); + } + if (key == null) { + throw new WebApplicationException(Response.Status.BAD_REQUEST); + } + if (rowModel.getCells().size() == 0) { + throw new WebApplicationException(Response.Status.BAD_REQUEST); + } + delete = new Delete(key); + + CellModel valueToDeleteCell = rowModel.getCells().get(0); + byte[] valueToDeleteColumn = valueToDeleteCell.getColumn(); + if (valueToDeleteColumn == null) { + try { + valueToDeleteColumn = rowspec.getColumns()[0]; + } catch (final ArrayIndexOutOfBoundsException e) { + throw new WebApplicationException(Response.Status.BAD_REQUEST); + } + } + byte[][] parts = KeyValue.parseColumn(valueToDeleteColumn); + if (parts.length == 2 && parts[1].length > 0) { + delete.deleteColumns(parts[0], parts[1]); + } else { + throw new WebApplicationException(Response.Status.BAD_REQUEST); + } + + table = pool.getTable(tableResource.getName()); + ((HTable) table).setAutoFlush(false); + boolean retValue = table.checkAndDelete(key, parts[0], parts[1], + valueToDeleteCell.getValue(), delete); + if (LOG.isDebugEnabled()) { + LOG.debug("CHECK-AND-DELETE " + delete.toString() + ", returns " + + retValue); + } + ((HTable) table).setAutoFlush(true); + table.flushCommits(); + ResponseBuilder response = Response.ok(); + if (!retValue) { + response = Response.status(304); + } + return response.build(); + } catch (final IOException e) { + throw new WebApplicationException(e, Response.Status.SERVICE_UNAVAILABLE); + } finally { + if (table != null) { + pool.putTable(table); + } + } + } + + @PUT + @Consumes({ MIMETYPE_XML, MIMETYPE_JSON, MIMETYPE_PROTOBUF }) + public Response put(final CellSetModel model, final @Context UriInfo uriInfo) { + if (LOG.isDebugEnabled()) { + LOG.debug("PUT " + uriInfo.getAbsolutePath()); + } + return update(model, true); + } + + @POST + @Consumes({ MIMETYPE_XML, MIMETYPE_JSON, MIMETYPE_PROTOBUF }) + public Response post(final CellSetModel model, final @Context UriInfo uriInfo) { + if (LOG.isDebugEnabled()) { + LOG.debug("POST " + uriInfo.getAbsolutePath()); + } + return update(model, false); + } +} Index: src/main/java/org/apache/hadoop/hbase/rest/ExistsResource.java =================================================================== --- src/main/java/org/apache/hadoop/hbase/rest/ExistsResource.java (revision 1215035) +++ src/main/java/org/apache/hadoop/hbase/rest/ExistsResource.java (working copy) @@ -22,7 +22,11 @@ import java.io.IOException; +import javax.ws.rs.Consumes; +import javax.ws.rs.DELETE; import javax.ws.rs.GET; +import javax.ws.rs.POST; +import javax.ws.rs.PUT; import javax.ws.rs.Produces; import javax.ws.rs.WebApplicationException; import javax.ws.rs.core.CacheControl; @@ -44,6 +48,7 @@ /** * Constructor + * * @param tableResource * @throws IOException */ @@ -53,11 +58,11 @@ } @GET - @Produces({MIMETYPE_TEXT, MIMETYPE_XML, MIMETYPE_JSON, MIMETYPE_PROTOBUF, - MIMETYPE_BINARY}) + @Produces({ MIMETYPE_TEXT, MIMETYPE_XML, MIMETYPE_JSON, MIMETYPE_PROTOBUF, + MIMETYPE_BINARY }) public Response get(final @Context UriInfo uriInfo) { try { - if (!servlet.getAdmin().tableExists(tableResource.table)) { + if (!servlet.getAdmin().tableExists(tableResource.getName())) { throw new WebApplicationException(Response.Status.NOT_FOUND); } } catch (IOException e) { Index: src/main/java/org/apache/hadoop/hbase/rest/RootResource.java =================================================================== --- src/main/java/org/apache/hadoop/hbase/rest/RootResource.java (revision 1215035) +++ src/main/java/org/apache/hadoop/hbase/rest/RootResource.java (working copy) @@ -101,4 +101,16 @@ final @PathParam("table") String table) throws IOException { return new TableResource(table); } + + @Path("checkandput/{table}") + public CheckAndPutTableResource getCheckAndPutTableResource( + final @PathParam("table") String table) throws IOException { + return new CheckAndPutTableResource(table); + } + + @Path("checkanddelete/{table}") + public CheckAndDeleteTableResource getCheckAndDeleteTableResource( + final @PathParam("table") String table) throws IOException { + return new CheckAndDeleteTableResource(table); + } } Index: src/main/java/org/apache/hadoop/hbase/rest/client/RemoteHTable.java =================================================================== --- src/main/java/org/apache/hadoop/hbase/rest/client/RemoteHTable.java (revision 1215035) +++ src/main/java/org/apache/hadoop/hbase/rest/client/RemoteHTable.java (working copy) @@ -29,12 +29,6 @@ import java.util.Set; import java.util.TreeMap; -import org.apache.commons.logging.Log; -import org.apache.commons.logging.LogFactory; - -import org.apache.hadoop.util.StringUtils; - -import org.apache.hadoop.conf.Configuration; import org.apache.hadoop.hbase.HBaseConfiguration; import org.apache.hadoop.hbase.HConstants; import org.apache.hadoop.hbase.HTableDescriptor; @@ -44,9 +38,9 @@ import org.apache.hadoop.hbase.client.HTableInterface; import org.apache.hadoop.hbase.client.Increment; import org.apache.hadoop.hbase.client.Put; -import org.apache.hadoop.hbase.client.Row; import org.apache.hadoop.hbase.client.Result; import org.apache.hadoop.hbase.client.ResultScanner; +import org.apache.hadoop.hbase.client.Row; import org.apache.hadoop.hbase.client.RowLock; import org.apache.hadoop.hbase.client.Scan; import org.apache.hadoop.hbase.io.TimeRange; @@ -58,6 +52,12 @@ import org.apache.hadoop.hbase.rest.model.TableSchemaModel; import org.apache.hadoop.hbase.util.Bytes; +import org.apache.hadoop.conf.Configuration; +import org.apache.hadoop.util.StringUtils; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + /** * HTable interface to remote tables accessed via REST gateway */ @@ -73,7 +73,7 @@ final long sleepTime; @SuppressWarnings("unchecked") - protected String buildRowSpec(final byte[] row, final Map familyMap, + protected String buildRowSpec(final byte[] row, final Map familyMap, final long startTime, final long endTime, final int maxVersions) { StringBuffer sb = new StringBuffer(); sb.append('/'); @@ -91,19 +91,19 @@ sb.append('/'); } while (i.hasNext()) { - Map.Entry e = (Map.Entry)i.next(); - Collection quals = (Collection)e.getValue(); + Map.Entry e = (Map.Entry) i.next(); + Collection quals = (Collection) e.getValue(); if (quals != null && !quals.isEmpty()) { Iterator ii = quals.iterator(); while (ii.hasNext()) { - sb.append(Bytes.toStringBinary((byte[])e.getKey())); + sb.append(Bytes.toStringBinary((byte[]) e.getKey())); sb.append(':'); Object o = ii.next(); // Puts use byte[] but Deletes use KeyValue if (o instanceof byte[]) { - sb.append(Bytes.toStringBinary((byte[])o)); + sb.append(Bytes.toStringBinary((byte[]) o)); } else if (o instanceof KeyValue) { - sb.append(Bytes.toStringBinary(((KeyValue)o).getQualifier())); + sb.append(Bytes.toStringBinary(((KeyValue) o).getQualifier())); } else { throw new RuntimeException("object type not handled"); } @@ -112,7 +112,7 @@ } } } else { - sb.append(Bytes.toStringBinary((byte[])e.getKey())); + sb.append(Bytes.toStringBinary((byte[]) e.getKey())); sb.append(':'); } if (i.hasNext()) { @@ -140,14 +140,14 @@ protected Result[] buildResultFromModel(final CellSetModel model) { List results = new ArrayList(); - for (RowModel row: model.getRows()) { + for (RowModel row : model.getRows()) { List kvs = new ArrayList(); - for (CellModel cell: row.getCells()) { + for (CellModel cell : row.getCells()) { byte[][] split = KeyValue.parseColumn(cell.getColumn()); byte[] column = split[0]; byte[] qualifier = split.length > 1 ? split[1] : null; - kvs.add(new KeyValue(row.getKey(), column, qualifier, - cell.getTimestamp(), cell.getValue())); + kvs.add(new KeyValue(row.getKey(), column, qualifier, cell + .getTimestamp(), cell.getValue())); } results.add(new Result(kvs)); } @@ -157,11 +157,11 @@ protected CellSetModel buildModelFromPut(Put put) { RowModel row = new RowModel(put.getRow()); long ts = put.getTimeStamp(); - for (List kvs: put.getFamilyMap().values()) { - for (KeyValue kv: kvs) { + for (List kvs : put.getFamilyMap().values()) { + for (KeyValue kv : kvs) { row.addCell(new CellModel(kv.getFamily(), kv.getQualifier(), - ts != HConstants.LATEST_TIMESTAMP ? ts : kv.getTimestamp(), - kv.getValue())); + ts != HConstants.LATEST_TIMESTAMP ? ts : kv.getTimestamp(), kv + .getValue())); } } CellSetModel model = new CellSetModel(); @@ -171,6 +171,7 @@ /** * Constructor + * * @param client * @param name */ @@ -180,6 +181,7 @@ /** * Constructor + * * @param client * @param name * @param accessToken @@ -190,6 +192,7 @@ /** * Constructor + * * @param client * @param conf * @param name @@ -202,6 +205,7 @@ /** * Constructor + * * @param conf */ public RemoteHTable(Client client, Configuration conf, byte[] name, @@ -233,17 +237,19 @@ sb.append('/'); sb.append("schema"); for (int i = 0; i < maxRetries; i++) { - Response response = client.get(sb.toString(), Constants.MIMETYPE_PROTOBUF); + Response response = client + .get(sb.toString(), Constants.MIMETYPE_PROTOBUF); int code = response.getCode(); switch (code) { case 200: TableSchemaModel schema = new TableSchemaModel(); schema.getObjectFromMessage(response.getBody()); return schema.getTableDescriptor(); - case 509: + case 509: try { Thread.sleep(sleepTime); - } catch (InterruptedException e) { } + } catch (InterruptedException e) { + } break; default: throw new IOException("schema request returned " + code); @@ -259,7 +265,7 @@ public Result get(Get get) throws IOException { TimeRange range = get.getTimeRange(); String spec = buildRowSpec(get.getRow(), get.getFamilyMap(), - range.getMin(), range.getMax(), get.getMaxVersions()); + range.getMin(), range.getMax(), get.getMaxVersions()); if (get.getFilter() != null) { LOG.warn("filters not supported on gets"); } @@ -283,7 +289,8 @@ case 509: try { Thread.sleep(sleepTime); - } catch (InterruptedException e) { } + } catch (InterruptedException e) { + } break; default: throw new IOException("get request returned " + code); @@ -304,14 +311,14 @@ sb.append('/'); if (accessToken != null) { sb.append(accessToken); - sb.append('/'); + sb.append('/'); } sb.append(Bytes.toStringBinary(name)); sb.append('/'); sb.append(Bytes.toStringBinary(put.getRow())); for (int i = 0; i < maxRetries; i++) { - Response response = client.put(sb.toString(), Constants.MIMETYPE_PROTOBUF, - model.createProtobufOutput()); + Response response = client.put(sb.toString(), + Constants.MIMETYPE_PROTOBUF, model.createProtobufOutput()); int code = response.getCode(); switch (code) { case 200: @@ -319,7 +326,8 @@ case 509: try { Thread.sleep(sleepTime); - } catch (InterruptedException e) { } + } catch (InterruptedException e) { + } break; default: throw new IOException("put request failed with " + code); @@ -333,25 +341,25 @@ // ignores the row specification in the URI // separate puts by row - TreeMap> map = - new TreeMap>(Bytes.BYTES_COMPARATOR); - for (Put put: puts) { + TreeMap> map = new TreeMap>( + Bytes.BYTES_COMPARATOR); + for (Put put : puts) { byte[] row = put.getRow(); List kvs = map.get(row); if (kvs == null) { kvs = new ArrayList(); map.put(row, kvs); } - for (List l: put.getFamilyMap().values()) { + for (List l : put.getFamilyMap().values()) { kvs.addAll(l); } } // build the cell set CellSetModel model = new CellSetModel(); - for (Map.Entry> e: map.entrySet()) { + for (Map.Entry> e : map.entrySet()) { RowModel row = new RowModel(e.getKey()); - for (KeyValue kv: e.getValue()) { + for (KeyValue kv : e.getValue()) { row.addCell(new CellModel(kv)); } model.addRow(row); @@ -362,13 +370,13 @@ sb.append('/'); if (accessToken != null) { sb.append(accessToken); - sb.append('/'); + sb.append('/'); } sb.append(Bytes.toStringBinary(name)); sb.append("/$multiput"); // can be any nonexistent row for (int i = 0; i < maxRetries; i++) { - Response response = client.put(sb.toString(), Constants.MIMETYPE_PROTOBUF, - model.createProtobufOutput()); + Response response = client.put(sb.toString(), + Constants.MIMETYPE_PROTOBUF, model.createProtobufOutput()); int code = response.getCode(); switch (code) { case 200: @@ -376,7 +384,8 @@ case 509: try { Thread.sleep(sleepTime); - } catch (InterruptedException e) { } + } catch (InterruptedException e) { + } break; default: throw new IOException("multiput request failed with " + code); @@ -387,7 +396,7 @@ public void delete(Delete delete) throws IOException { String spec = buildRowSpec(delete.getRow(), delete.getFamilyMap(), - delete.getTimeStamp(), delete.getTimeStamp(), 1); + delete.getTimeStamp(), delete.getTimeStamp(), 1); for (int i = 0; i < maxRetries; i++) { Response response = client.delete(spec); int code = response.getCode(); @@ -397,7 +406,8 @@ case 509: try { Thread.sleep(sleepTime); - } catch (InterruptedException e) { } + } catch (InterruptedException e) { + } break; default: throw new IOException("delete request failed with " + code); @@ -407,7 +417,7 @@ } public void delete(List deletes) throws IOException { - for (Delete delete: deletes) { + for (Delete delete : deletes) { delete(delete); } } @@ -438,7 +448,7 @@ sb.append("scanner"); for (int i = 0; i < maxRetries; i++) { Response response = client.post(sb.toString(), - Constants.MIMETYPE_PROTOBUF, model.createProtobufOutput()); + Constants.MIMETYPE_PROTOBUF, model.createProtobufOutput()); int code = response.getCode(); switch (code) { case 201: @@ -447,7 +457,8 @@ case 509: try { Thread.sleep(sleepTime); - } catch (InterruptedException e) { } + } catch (InterruptedException e) { + } break; default: throw new IOException("scan request failed with " + code); @@ -463,7 +474,7 @@ sb.append(nbRows); for (int i = 0; i < maxRetries; i++) { Response response = client.get(sb.toString(), - Constants.MIMETYPE_PROTOBUF); + Constants.MIMETYPE_PROTOBUF); int code = response.getCode(); switch (code) { case 200: @@ -476,7 +487,8 @@ case 509: try { Thread.sleep(sleepTime); - } catch (InterruptedException e) { } + } catch (InterruptedException e) { + } break; default: throw new IOException("scanner.next request failed with " + code); @@ -493,7 +505,7 @@ } return results[0]; } - + class Iter implements Iterator { Result cache; @@ -527,7 +539,7 @@ public void remove() { throw new RuntimeException("remove() not supported"); } - + } @Override @@ -581,12 +593,82 @@ public boolean checkAndPut(byte[] row, byte[] family, byte[] qualifier, byte[] value, Put put) throws IOException { - throw new IOException("checkAndPut not supported"); + // column to check-the-value + put.add(new KeyValue(row, family, qualifier, value)); + + CellSetModel model = buildModelFromPut(put); + StringBuilder sb = new StringBuilder(); + sb.append('/'); + if (accessToken != null) { + sb.append(accessToken); + sb.append('/'); + } + sb.append("checkandput"); + sb.append('/'); + sb.append(Bytes.toStringBinary(name)); + sb.append('/'); + sb.append(Bytes.toStringBinary(put.getRow())); + + for (int i = 0; i < maxRetries; i++) { + Response response = client.put(sb.toString(), + Constants.MIMETYPE_PROTOBUF, model.createProtobufOutput()); + int code = response.getCode(); + switch (code) { + case 200: + return true; + case 304: // NOT-MODIFIED + return false; + case 509: + try { + Thread.sleep(sleepTime); + } catch (final InterruptedException e) { + } + break; + default: + throw new IOException("checkAndPut request failed with " + code); + } + } + throw new IOException("checkAndPut request timed out"); } public boolean checkAndDelete(byte[] row, byte[] family, byte[] qualifier, byte[] value, Delete delete) throws IOException { - throw new IOException("checkAndDelete not supported"); + Put put = new Put(row); + // column to check-the-value + put.add(new KeyValue(row, family, qualifier, value)); + CellSetModel model = buildModelFromPut(put); + StringBuilder sb = new StringBuilder(); + sb.append('/'); + if (accessToken != null) { + sb.append(accessToken); + sb.append('/'); + } + sb.append("checkanddelete"); + sb.append('/'); + sb.append(Bytes.toStringBinary(name)); + sb.append('/'); + sb.append(Bytes.toStringBinary(row)); + + for (int i = 0; i < maxRetries; i++) { + Response response = client.put(sb.toString(), + Constants.MIMETYPE_PROTOBUF, model.createProtobufOutput()); + int code = response.getCode(); + switch (code) { + case 200: + return true; + case 304: // NOT-MODIFIED + return false; + case 509: + try { + Thread.sleep(sleepTime); + } catch (final InterruptedException e) { + } + break; + default: + throw new IOException("checkAndDelete request failed with " + code); + } + } + throw new IOException("checkAndDelete request timed out"); } public Result increment(Increment increment) throws IOException {