1   /*
2    * Copyright 2010 The Apache Software Foundation
3    *
4    * Licensed to the Apache Software Foundation (ASF) under one
5    * or more contributor license agreements.  See the NOTICE file
6    * distributed with this work for additional information
7    * regarding copyright ownership.  The ASF licenses this file
8    * to you under the Apache License, Version 2.0 (the
9    * "License"); you may not use this file except in compliance
10   * with the License.  You may obtain a copy of the License at
11   *
12   *     http://www.apache.org/licenses/LICENSE-2.0
13   *
14   * Unless required by applicable law or agreed to in writing, software
15   * distributed under the License is distributed on an "AS IS" BASIS,
16   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
17   * See the License for the specific language governing permissions and
18   * limitations under the License.
19   */
20  
21  package org.apache.hadoop.hbase.rest;
22  
23  import java.io.ByteArrayInputStream;
24  import java.io.IOException;
25  import java.io.StringWriter;
26  import java.net.URLEncoder;
27  
28  import javax.xml.bind.JAXBContext;
29  import javax.xml.bind.JAXBException;
30  import javax.xml.bind.Marshaller;
31  import javax.xml.bind.Unmarshaller;
32  
33  import org.apache.commons.httpclient.Header;
34  import org.apache.hadoop.conf.Configuration;
35  import org.apache.hadoop.hbase.HBaseTestingUtility;
36  import org.apache.hadoop.hbase.HColumnDescriptor;
37  import org.apache.hadoop.hbase.HConstants;
38  import org.apache.hadoop.hbase.HTableDescriptor;
39  import org.apache.hadoop.hbase.client.HBaseAdmin;
40  import org.apache.hadoop.hbase.rest.client.Client;
41  import org.apache.hadoop.hbase.rest.client.Cluster;
42  import org.apache.hadoop.hbase.rest.client.Response;
43  import org.apache.hadoop.hbase.rest.model.CellModel;
44  import org.apache.hadoop.hbase.rest.model.CellSetModel;
45  import org.apache.hadoop.hbase.rest.model.RowModel;
46  import org.apache.hadoop.hbase.util.Bytes;
47  
48  import static org.junit.Assert.*;
49  import org.junit.AfterClass;
50  import org.junit.BeforeClass;
51  import org.junit.Test;
52  
53  public class TestRowResource {
54    private static final String TABLE = "TestRowResource";
55    private static final String CFA = "a";
56    private static final String CFB = "b";
57    private static final String COLUMN_1 = CFA + ":1";
58    private static final String COLUMN_2 = CFB + ":2";
59    private static final String ROW_1 = "testrow1";
60    private static final String VALUE_1 = "testvalue1";
61    private static final String ROW_2 = "testrow2";
62    private static final String VALUE_2 = "testvalue2";
63    private static final String ROW_3 = "testrow3";
64    private static final String VALUE_3 = "testvalue3";
65    private static final String ROW_4 = "testrow4";
66    private static final String VALUE_4 = "testvalue4";
67  
68    private static final HBaseTestingUtility TEST_UTIL = new HBaseTestingUtility();
69    private static final HBaseRESTTestingUtility REST_TEST_UTIL = 
70      new HBaseRESTTestingUtility();
71    private static Client client;
72    private static JAXBContext context;
73    private static Marshaller marshaller;
74    private static Unmarshaller unmarshaller;
75    private static Configuration conf;
76  
77    @BeforeClass
78    public static void setUpBeforeClass() throws Exception {
79      conf = TEST_UTIL.getConfiguration();
80      TEST_UTIL.startMiniCluster(3);
81      REST_TEST_UTIL.startServletContainer(conf);
82      context = JAXBContext.newInstance(
83          CellModel.class,
84          CellSetModel.class,
85          RowModel.class);
86      marshaller = context.createMarshaller();
87      unmarshaller = context.createUnmarshaller();
88      client = new Client(new Cluster().add("localhost", 
89        REST_TEST_UTIL.getServletPort()));
90      HBaseAdmin admin = TEST_UTIL.getHBaseAdmin();
91      if (admin.tableExists(TABLE)) {
92        return;
93      }
94      HTableDescriptor htd = new HTableDescriptor(TABLE);
95      htd.addFamily(new HColumnDescriptor(CFA));
96      htd.addFamily(new HColumnDescriptor(CFB));
97      admin.createTable(htd);
98    }
99  
100   @AfterClass
101   public static void tearDownAfterClass() throws Exception {
102     REST_TEST_UTIL.shutdownServletContainer();
103     TEST_UTIL.shutdownMiniCluster();
104   }
105 
106   private static Response deleteRow(String table, String row) 
107       throws IOException {
108     StringBuilder path = new StringBuilder();
109     path.append('/');
110     path.append(table);
111     path.append('/');
112     path.append(row);
113     Response response = client.delete(path.toString());
114     Thread.yield();
115     return response;
116   }
117 
118   private static Response deleteValue(String table, String row, String column)
119       throws IOException {
120     StringBuilder path = new StringBuilder();
121     path.append('/');
122     path.append(table);
123     path.append('/');
124     path.append(row);
125     path.append('/');
126     path.append(column);
127     Response response = client.delete(path.toString());
128     Thread.yield();
129     return response;
130   }
131 
132   private static Response getValueXML(String table, String row, String column)
133       throws IOException {
134     StringBuilder path = new StringBuilder();
135     path.append('/');
136     path.append(table);
137     path.append('/');
138     path.append(row);
139     path.append('/');
140     path.append(column);
141     return getValueXML(path.toString());
142   }
143 
144   private static Response getValueXML(String url) throws IOException {
145     Response response = client.get(url, Constants.MIMETYPE_XML);
146     return response;
147   }
148 
149   private static Response getValuePB(String table, String row, String column) 
150       throws IOException {
151     StringBuilder path = new StringBuilder();
152     path.append('/');
153     path.append(table);
154     path.append('/');
155     path.append(row);
156     path.append('/');
157     path.append(column);
158     return getValuePB(path.toString());
159   }
160 
161   private static Response getValuePB(String url) throws IOException {
162     Response response = client.get(url, Constants.MIMETYPE_PROTOBUF); 
163     return response;
164   }
165 
166   private static Response putValueXML(String table, String row, String column,
167       String value) throws IOException, JAXBException {
168     StringBuilder path = new StringBuilder();
169     path.append('/');
170     path.append(table);
171     path.append('/');
172     path.append(row);
173     path.append('/');
174     path.append(column);
175     return putValueXML(path.toString(), table, row, column, value);
176   }
177 
178   private static Response putValueXML(String url, String table, String row,
179       String column, String value) throws IOException, JAXBException {
180     RowModel rowModel = new RowModel(row);
181     rowModel.addCell(new CellModel(Bytes.toBytes(column),
182       Bytes.toBytes(value)));
183     CellSetModel cellSetModel = new CellSetModel();
184     cellSetModel.addRow(rowModel);
185     StringWriter writer = new StringWriter();
186     marshaller.marshal(cellSetModel, writer);
187     Response response = client.put(url, Constants.MIMETYPE_XML,
188       Bytes.toBytes(writer.toString()));
189     Thread.yield();
190     return response;
191   }
192 
193   private static void checkValueXML(String table, String row, String column,
194       String value) throws IOException, JAXBException {
195     Response response = getValueXML(table, row, column);
196     assertEquals(response.getCode(), 200);
197     CellSetModel cellSet = (CellSetModel)
198       unmarshaller.unmarshal(new ByteArrayInputStream(response.getBody()));
199     RowModel rowModel = cellSet.getRows().get(0);
200     CellModel cell = rowModel.getCells().get(0);
201     assertEquals(Bytes.toString(cell.getColumn()), column);
202     assertEquals(Bytes.toString(cell.getValue()), value);
203   }
204 
205   private static void checkValueXML(String url, String table, String row,
206       String column, String value) throws IOException, JAXBException {
207     Response response = getValueXML(url);
208     assertEquals(response.getCode(), 200);
209     CellSetModel cellSet = (CellSetModel)
210       unmarshaller.unmarshal(new ByteArrayInputStream(response.getBody()));
211     RowModel rowModel = cellSet.getRows().get(0);
212     CellModel cell = rowModel.getCells().get(0);
213     assertEquals(Bytes.toString(cell.getColumn()), column);
214     assertEquals(Bytes.toString(cell.getValue()), value);
215   }
216 
217   private static Response putValuePB(String table, String row, String column,
218       String value) throws IOException {
219     StringBuilder path = new StringBuilder();
220     path.append('/');
221     path.append(table);
222     path.append('/');
223     path.append(row);
224     path.append('/');
225     path.append(column);
226     return putValuePB(path.toString(), table, row, column, value);
227   }
228 
229   private static Response putValuePB(String url, String table, String row,
230       String column, String value) throws IOException {
231     RowModel rowModel = new RowModel(row);
232     rowModel.addCell(new CellModel(Bytes.toBytes(column),
233       Bytes.toBytes(value)));
234     CellSetModel cellSetModel = new CellSetModel();
235     cellSetModel.addRow(rowModel);
236     Response response = client.put(url, Constants.MIMETYPE_PROTOBUF,
237       cellSetModel.createProtobufOutput());
238     Thread.yield();
239     return response;
240   }
241 
242   private static void checkValuePB(String table, String row, String column, 
243       String value) throws IOException {
244     Response response = getValuePB(table, row, column);
245     assertEquals(response.getCode(), 200);
246     CellSetModel cellSet = new CellSetModel();
247     cellSet.getObjectFromMessage(response.getBody());
248     RowModel rowModel = cellSet.getRows().get(0);
249     CellModel cell = rowModel.getCells().get(0);
250     assertEquals(Bytes.toString(cell.getColumn()), column);
251     assertEquals(Bytes.toString(cell.getValue()), value);
252   }
253 
254   @Test
255   public void testDelete() throws IOException, JAXBException {
256     Response response;
257     
258     response = putValueXML(TABLE, ROW_1, COLUMN_1, VALUE_1);
259     assertEquals(response.getCode(), 200);
260     response = putValueXML(TABLE, ROW_1, COLUMN_2, VALUE_2);
261     assertEquals(response.getCode(), 200);
262     checkValueXML(TABLE, ROW_1, COLUMN_1, VALUE_1);
263     checkValueXML(TABLE, ROW_1, COLUMN_2, VALUE_2);
264 
265     response = deleteValue(TABLE, ROW_1, COLUMN_1);
266     assertEquals(response.getCode(), 200);
267     response = getValueXML(TABLE, ROW_1, COLUMN_1);
268     assertEquals(response.getCode(), 404);
269     checkValueXML(TABLE, ROW_1, COLUMN_2, VALUE_2);
270 
271     response = deleteRow(TABLE, ROW_1);
272     assertEquals(response.getCode(), 200);    
273     response = getValueXML(TABLE, ROW_1, COLUMN_1);
274     assertEquals(response.getCode(), 404);
275     response = getValueXML(TABLE, ROW_1, COLUMN_2);
276     assertEquals(response.getCode(), 404);
277   }
278 
279   @Test
280   public void testForbidden() throws IOException, JAXBException {
281     Response response;
282 
283     conf.set("hbase.rest.readonly", "true");
284 
285     response = putValueXML(TABLE, ROW_1, COLUMN_1, VALUE_1);
286     assertEquals(response.getCode(), 403);
287     response = putValuePB(TABLE, ROW_1, COLUMN_1, VALUE_1);
288     assertEquals(response.getCode(), 403);
289     response = deleteValue(TABLE, ROW_1, COLUMN_1);
290     assertEquals(response.getCode(), 403);
291     response = deleteRow(TABLE, ROW_1);
292     assertEquals(response.getCode(), 403);
293 
294     conf.set("hbase.rest.readonly", "false");
295 
296     response = putValueXML(TABLE, ROW_1, COLUMN_1, VALUE_1);
297     assertEquals(response.getCode(), 200);
298     response = putValuePB(TABLE, ROW_1, COLUMN_1, VALUE_1);
299     assertEquals(response.getCode(), 200);
300     response = deleteValue(TABLE, ROW_1, COLUMN_1);
301     assertEquals(response.getCode(), 200);
302     response = deleteRow(TABLE, ROW_1);
303     assertEquals(response.getCode(), 200);
304   }
305 
306   @Test
307   public void testSingleCellGetPutXML() throws IOException, JAXBException {
308     Response response = getValueXML(TABLE, ROW_1, COLUMN_1);
309     assertEquals(response.getCode(), 404);
310 
311     response = putValueXML(TABLE, ROW_1, COLUMN_1, VALUE_1);
312     assertEquals(response.getCode(), 200);
313     checkValueXML(TABLE, ROW_1, COLUMN_1, VALUE_1);
314     response = putValueXML(TABLE, ROW_1, COLUMN_1, VALUE_2);
315     assertEquals(response.getCode(), 200);
316     checkValueXML(TABLE, ROW_1, COLUMN_1, VALUE_2);
317 
318     response = deleteRow(TABLE, ROW_1);
319     assertEquals(response.getCode(), 200);
320   }
321 
322   @Test
323   public void testSingleCellGetPutPB() throws IOException, JAXBException {
324     Response response = getValuePB(TABLE, ROW_1, COLUMN_1);
325     assertEquals(response.getCode(), 404);
326 
327     response = putValuePB(TABLE, ROW_1, COLUMN_1, VALUE_1);
328     assertEquals(response.getCode(), 200);
329     checkValuePB(TABLE, ROW_1, COLUMN_1, VALUE_1);
330 
331     response = putValuePB(TABLE, ROW_1, COLUMN_1, VALUE_1);
332     assertEquals(response.getCode(), 200);
333     checkValuePB(TABLE, ROW_1, COLUMN_1, VALUE_1);
334     response = putValueXML(TABLE, ROW_1, COLUMN_1, VALUE_2);
335     assertEquals(response.getCode(), 200);
336     checkValuePB(TABLE, ROW_1, COLUMN_1, VALUE_2);
337 
338     response = deleteRow(TABLE, ROW_1);
339     assertEquals(response.getCode(), 200);
340   }
341 
342   @Test
343   public void testSingleCellGetPutBinary() throws IOException {
344     final String path = "/" + TABLE + "/" + ROW_3 + "/" + COLUMN_1;
345     final byte[] body = Bytes.toBytes(VALUE_3);
346     Response response = client.put(path, Constants.MIMETYPE_BINARY, body);
347     assertEquals(response.getCode(), 200);
348     Thread.yield();
349 
350     response = client.get(path, Constants.MIMETYPE_BINARY);
351     assertEquals(response.getCode(), 200);
352     assertTrue(Bytes.equals(response.getBody(), body));
353     boolean foundTimestampHeader = false;
354     for (Header header: response.getHeaders()) {
355       if (header.getName().equals("X-Timestamp")) {
356         foundTimestampHeader = true;
357         break;
358       }
359     }
360     assertTrue(foundTimestampHeader);
361 
362     response = deleteRow(TABLE, ROW_3);
363     assertEquals(response.getCode(), 200);
364   }
365 
366   @Test
367   public void testSingleCellGetJSON() throws IOException, JAXBException {
368     final String path = "/" + TABLE + "/" + ROW_4 + "/" + COLUMN_1;
369     Response response = client.put(path, Constants.MIMETYPE_BINARY,
370       Bytes.toBytes(VALUE_4));
371     assertEquals(response.getCode(), 200);
372     Thread.yield();
373     response = client.get(path, Constants.MIMETYPE_JSON);
374     assertEquals(response.getCode(), 200);
375     response = deleteRow(TABLE, ROW_4);
376     assertEquals(response.getCode(), 200);
377   }
378 
379   @Test
380   public void testURLEncodedKey() throws IOException, JAXBException {
381     String urlKey = "http://example.com/foo";
382     StringBuilder path = new StringBuilder();
383     path.append('/');
384     path.append(TABLE);
385     path.append('/');
386     path.append(URLEncoder.encode(urlKey, HConstants.UTF8_ENCODING));
387     path.append('/');
388     path.append(COLUMN_1);
389     Response response;
390     response = putValueXML(path.toString(), TABLE, urlKey, COLUMN_1,
391       VALUE_1);
392     assertEquals(response.getCode(), 200);
393     checkValueXML(path.toString(), TABLE, urlKey, COLUMN_1, VALUE_1);
394   }
395 
396   @Test
397   public void testNoSuchCF() throws IOException, JAXBException {
398     final String goodPath = "/" + TABLE + "/" + ROW_1 + "/" + CFA+":";
399     final String badPath = "/" + TABLE + "/" + ROW_1 + "/" + "BAD";
400     Response response = client.post(goodPath, Constants.MIMETYPE_BINARY,
401       Bytes.toBytes(VALUE_1));
402     assertEquals(response.getCode(), 200);
403     assertEquals(client.get(goodPath, Constants.MIMETYPE_BINARY).getCode(),
404       200);
405     assertEquals(client.get(badPath, Constants.MIMETYPE_BINARY).getCode(),
406       404);
407     assertEquals(client.get(goodPath, Constants.MIMETYPE_BINARY).getCode(),
408       200);
409   }
410 
411   @Test
412   public void testMultiCellGetPutXML() throws IOException, JAXBException {
413     String path = "/" + TABLE + "/fakerow";  // deliberate nonexistent row
414 
415     CellSetModel cellSetModel = new CellSetModel();
416     RowModel rowModel = new RowModel(ROW_1);
417     rowModel.addCell(new CellModel(Bytes.toBytes(COLUMN_1),
418       Bytes.toBytes(VALUE_1)));
419     rowModel.addCell(new CellModel(Bytes.toBytes(COLUMN_2),
420       Bytes.toBytes(VALUE_2)));
421     cellSetModel.addRow(rowModel);
422     rowModel = new RowModel(ROW_2);
423     rowModel.addCell(new CellModel(Bytes.toBytes(COLUMN_1),
424       Bytes.toBytes(VALUE_3)));
425     rowModel.addCell(new CellModel(Bytes.toBytes(COLUMN_2),
426       Bytes.toBytes(VALUE_4)));
427     cellSetModel.addRow(rowModel);
428     StringWriter writer = new StringWriter();
429     marshaller.marshal(cellSetModel, writer);
430     Response response = client.put(path, Constants.MIMETYPE_XML,
431       Bytes.toBytes(writer.toString()));
432     Thread.yield();
433 
434     // make sure the fake row was not actually created
435     response = client.get(path, Constants.MIMETYPE_XML);
436     assertEquals(response.getCode(), 404);
437 
438     // check that all of the values were created
439     checkValueXML(TABLE, ROW_1, COLUMN_1, VALUE_1);
440     checkValueXML(TABLE, ROW_1, COLUMN_2, VALUE_2);
441     checkValueXML(TABLE, ROW_2, COLUMN_1, VALUE_3);
442     checkValueXML(TABLE, ROW_2, COLUMN_2, VALUE_4);
443 
444     response = deleteRow(TABLE, ROW_1);
445     assertEquals(response.getCode(), 200);
446     response = deleteRow(TABLE, ROW_2);
447     assertEquals(response.getCode(), 200);
448   }
449 
450   @Test
451   public void testMultiCellGetPutPB() throws IOException {
452     String path = "/" + TABLE + "/fakerow";  // deliberate nonexistent row
453 
454     CellSetModel cellSetModel = new CellSetModel();
455     RowModel rowModel = new RowModel(ROW_1);
456     rowModel.addCell(new CellModel(Bytes.toBytes(COLUMN_1),
457       Bytes.toBytes(VALUE_1)));
458     rowModel.addCell(new CellModel(Bytes.toBytes(COLUMN_2),
459       Bytes.toBytes(VALUE_2)));
460     cellSetModel.addRow(rowModel);
461     rowModel = new RowModel(ROW_2);
462     rowModel.addCell(new CellModel(Bytes.toBytes(COLUMN_1),
463       Bytes.toBytes(VALUE_3)));
464     rowModel.addCell(new CellModel(Bytes.toBytes(COLUMN_2),
465       Bytes.toBytes(VALUE_4)));
466     cellSetModel.addRow(rowModel);
467     Response response = client.put(path, Constants.MIMETYPE_PROTOBUF,
468       cellSetModel.createProtobufOutput());
469     Thread.yield();
470 
471     // make sure the fake row was not actually created
472     response = client.get(path, Constants.MIMETYPE_PROTOBUF);
473     assertEquals(response.getCode(), 404);
474 
475     // check that all of the values were created
476     checkValuePB(TABLE, ROW_1, COLUMN_1, VALUE_1);
477     checkValuePB(TABLE, ROW_1, COLUMN_2, VALUE_2);
478     checkValuePB(TABLE, ROW_2, COLUMN_1, VALUE_3);
479     checkValuePB(TABLE, ROW_2, COLUMN_2, VALUE_4);
480 
481     response = deleteRow(TABLE, ROW_1);
482     assertEquals(response.getCode(), 200);
483     response = deleteRow(TABLE, ROW_2);
484     assertEquals(response.getCode(), 200);
485   }
486 }