View Javadoc

1   /*
2    *
3    * Licensed to the Apache Software Foundation (ASF) under one
4    * or more contributor license agreements.  See the NOTICE file
5    * distributed with this work for additional information
6    * regarding copyright ownership.  The ASF licenses this file
7    * to you under the Apache License, Version 2.0 (the
8    * "License"); you may not use this file except in compliance
9    * with the License.  You may obtain a copy of the License at
10   *
11   *     http://www.apache.org/licenses/LICENSE-2.0
12   *
13   * Unless required by applicable law or agreed to in writing, software
14   * distributed under the License is distributed on an "AS IS" BASIS,
15   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16   * See the License for the specific language governing permissions and
17   * limitations under the License.
18   */
19  
20  package org.apache.hadoop.hbase.rest;
21  
22  import java.io.IOException;
23  import java.util.ArrayList;
24  import java.util.List;
25  
26  import javax.ws.rs.Consumes;
27  import javax.ws.rs.DELETE;
28  import javax.ws.rs.GET;
29  import javax.ws.rs.POST;
30  import javax.ws.rs.PUT;
31  import javax.ws.rs.Produces;
32  import javax.ws.rs.core.Context;
33  import javax.ws.rs.core.HttpHeaders;
34  import javax.ws.rs.core.Response;
35  import javax.ws.rs.core.UriInfo;
36  import javax.ws.rs.core.Response.ResponseBuilder;
37  
38  import org.apache.commons.logging.Log;
39  import org.apache.commons.logging.LogFactory;
40  import org.apache.hadoop.classification.InterfaceAudience;
41  import org.apache.hadoop.hbase.HConstants;
42  import org.apache.hadoop.hbase.KeyValue;
43  import org.apache.hadoop.hbase.exceptions.TableNotFoundException;
44  import org.apache.hadoop.hbase.client.Delete;
45  import org.apache.hadoop.hbase.client.HTableInterface;
46  import org.apache.hadoop.hbase.client.HTablePool;
47  import org.apache.hadoop.hbase.client.Put;
48  import org.apache.hadoop.hbase.rest.model.CellModel;
49  import org.apache.hadoop.hbase.rest.model.CellSetModel;
50  import org.apache.hadoop.hbase.rest.model.RowModel;
51  import org.apache.hadoop.hbase.util.Bytes;
52  
53  @InterfaceAudience.Private
54  public class RowResource extends ResourceBase {
55    private static final Log LOG = LogFactory.getLog(RowResource.class);
56  
57    static final String CHECK_PUT = "put";
58    static final String CHECK_DELETE = "delete";
59  
60    TableResource tableResource;
61    RowSpec rowspec;
62    private String check = null;
63  
64    /**
65     * Constructor
66     * @param tableResource
67     * @param rowspec
68     * @param versions
69     * @throws IOException
70     */
71    public RowResource(TableResource tableResource, String rowspec,
72        String versions, String check) throws IOException {
73      super();
74      this.tableResource = tableResource;
75      this.rowspec = new RowSpec(rowspec);
76      if (versions != null) {
77        this.rowspec.setMaxVersions(Integer.valueOf(versions));
78      }
79      this.check = check;
80    }
81  
82    @GET
83    @Produces({MIMETYPE_XML, MIMETYPE_JSON, MIMETYPE_PROTOBUF,
84      MIMETYPE_PROTOBUF_IETF})
85    public Response get(final @Context UriInfo uriInfo) {
86      if (LOG.isDebugEnabled()) {
87        LOG.debug("GET " + uriInfo.getAbsolutePath());
88      }
89      servlet.getMetrics().incrementRequests(1);
90      try {
91        ResultGenerator generator =
92          ResultGenerator.fromRowSpec(tableResource.getName(), rowspec, null);
93        if (!generator.hasNext()) {
94          return Response.status(Response.Status.NOT_FOUND)
95            .type(MIMETYPE_TEXT).entity("Not found" + CRLF)
96            .build();
97        }
98        int count = 0;
99        CellSetModel model = new CellSetModel();
100       KeyValue value = generator.next();
101       byte[] rowKey = value.getRow();
102       RowModel rowModel = new RowModel(rowKey);
103       do {
104         if (!Bytes.equals(value.getRow(), rowKey)) {
105           model.addRow(rowModel);
106           rowKey = value.getRow();
107           rowModel = new RowModel(rowKey);
108         }
109         rowModel.addCell(new CellModel(value.getFamily(), value.getQualifier(),
110           value.getTimestamp(), value.getValue()));
111         if (++count > rowspec.getMaxValues()) {
112           break;
113         }
114         value = generator.next();
115       } while (value != null);
116       model.addRow(rowModel);
117       servlet.getMetrics().incrementSucessfulGetRequests(1);
118       return Response.ok(model).build();
119     } catch (RuntimeException e) {
120       servlet.getMetrics().incrementFailedPutRequests(1);
121       if (e.getCause() instanceof TableNotFoundException) {
122         return Response.status(Response.Status.NOT_FOUND)
123           .type(MIMETYPE_TEXT).entity("Not found" + CRLF)
124           .build();
125       }
126       return Response.status(Response.Status.BAD_REQUEST)
127         .type(MIMETYPE_TEXT).entity("Bad request" + CRLF)
128         .build();
129     } catch (Exception e) {
130       servlet.getMetrics().incrementFailedPutRequests(1);
131       return Response.status(Response.Status.SERVICE_UNAVAILABLE)
132         .type(MIMETYPE_TEXT).entity("Unavailable" + CRLF)
133         .build();
134     }
135   }
136 
137   @GET
138   @Produces(MIMETYPE_BINARY)
139   public Response getBinary(final @Context UriInfo uriInfo) {
140     if (LOG.isDebugEnabled()) {
141       LOG.debug("GET " + uriInfo.getAbsolutePath() + " as "+ MIMETYPE_BINARY);
142     }
143     servlet.getMetrics().incrementRequests(1);
144     // doesn't make sense to use a non specific coordinate as this can only
145     // return a single cell
146     if (!rowspec.hasColumns() || rowspec.getColumns().length > 1) {
147       return Response.status(Response.Status.BAD_REQUEST)
148         .type(MIMETYPE_TEXT).entity("Bad request" + CRLF)
149         .build();
150     }
151     try {
152       ResultGenerator generator =
153         ResultGenerator.fromRowSpec(tableResource.getName(), rowspec, null);
154       if (!generator.hasNext()) {
155         return Response.status(Response.Status.NOT_FOUND)
156           .type(MIMETYPE_TEXT).entity("Not found" + CRLF)
157           .build();
158       }
159       KeyValue value = generator.next();
160       ResponseBuilder response = Response.ok(value.getValue());
161       response.header("X-Timestamp", value.getTimestamp());
162       servlet.getMetrics().incrementSucessfulGetRequests(1);
163       return response.build();
164     } catch (IOException e) {
165       servlet.getMetrics().incrementFailedGetRequests(1);
166       return Response.status(Response.Status.SERVICE_UNAVAILABLE)
167         .type(MIMETYPE_TEXT).entity("Unavailable" + CRLF)
168         .build();
169     }
170   }
171 
172   Response update(final CellSetModel model, final boolean replace) {
173     servlet.getMetrics().incrementRequests(1);
174     if (servlet.isReadOnly()) {
175       return Response.status(Response.Status.FORBIDDEN)
176         .type(MIMETYPE_TEXT).entity("Forbidden" + CRLF)
177         .build();
178     }
179 
180     if (CHECK_PUT.equalsIgnoreCase(check)) {
181       return checkAndPut(model);
182     } else if (CHECK_DELETE.equalsIgnoreCase(check)) {
183       return checkAndDelete(model);
184     } else if (check != null && check.length() > 0) {
185       LOG.warn("Unknown check value: " + check + ", ignored");
186     }
187 
188     HTablePool pool = servlet.getTablePool();
189     HTableInterface table = null;
190     try {
191       List<RowModel> rows = model.getRows();
192       List<Put> puts = new ArrayList<Put>();
193       for (RowModel row: rows) {
194         byte[] key = row.getKey();
195         if (key == null) {
196           key = rowspec.getRow();
197         }
198         if (key == null) {
199           return Response.status(Response.Status.BAD_REQUEST)
200             .type(MIMETYPE_TEXT).entity("Bad request" + CRLF)
201             .build();
202         }
203         Put put = new Put(key);
204         int i = 0;
205         for (CellModel cell: row.getCells()) {
206           byte[] col = cell.getColumn();
207           if (col == null) try {
208             col = rowspec.getColumns()[i++];
209           } catch (ArrayIndexOutOfBoundsException e) {
210             col = null;
211           }
212           if (col == null) {
213             return Response.status(Response.Status.BAD_REQUEST)
214               .type(MIMETYPE_TEXT).entity("Bad request" + CRLF)
215               .build();
216           }
217           byte [][] parts = KeyValue.parseColumn(col);
218           if (parts.length == 2 && parts[1].length > 0) {
219             put.add(parts[0], parts[1], cell.getTimestamp(), cell.getValue());
220           } else {
221             put.add(parts[0], null, cell.getTimestamp(), cell.getValue());
222           }
223         }
224         puts.add(put);
225         if (LOG.isDebugEnabled()) {
226           LOG.debug("PUT " + put.toString());
227         }
228       }
229       table = pool.getTable(tableResource.getName());
230       table.put(puts);
231       table.flushCommits();
232       ResponseBuilder response = Response.ok();
233       servlet.getMetrics().incrementSucessfulPutRequests(1);
234       return response.build();
235     } catch (IOException e) {
236       servlet.getMetrics().incrementFailedPutRequests(1);
237       return Response.status(Response.Status.SERVICE_UNAVAILABLE)
238         .type(MIMETYPE_TEXT).entity("Unavailable" + CRLF)
239         .build();
240     } finally {
241       if (table != null) try {
242         table.close();
243       } catch (IOException ioe) {
244         LOG.debug("Exception received while closing the table", ioe);
245       }
246     }
247   }
248 
249   // This currently supports only update of one row at a time.
250   Response updateBinary(final byte[] message, final HttpHeaders headers,
251       final boolean replace) {
252     servlet.getMetrics().incrementRequests(1);
253     if (servlet.isReadOnly()) {
254       return Response.status(Response.Status.FORBIDDEN)
255         .type(MIMETYPE_TEXT).entity("Forbidden" + CRLF)
256         .build();
257     }
258     HTablePool pool = servlet.getTablePool();
259     HTableInterface table = null;
260     try {
261       byte[] row = rowspec.getRow();
262       byte[][] columns = rowspec.getColumns();
263       byte[] column = null;
264       if (columns != null) {
265         column = columns[0];
266       }
267       long timestamp = HConstants.LATEST_TIMESTAMP;
268       List<String> vals = headers.getRequestHeader("X-Row");
269       if (vals != null && !vals.isEmpty()) {
270         row = Bytes.toBytes(vals.get(0));
271       }
272       vals = headers.getRequestHeader("X-Column");
273       if (vals != null && !vals.isEmpty()) {
274         column = Bytes.toBytes(vals.get(0));
275       }
276       vals = headers.getRequestHeader("X-Timestamp");
277       if (vals != null && !vals.isEmpty()) {
278         timestamp = Long.valueOf(vals.get(0));
279       }
280       if (column == null) {
281         return Response.status(Response.Status.BAD_REQUEST)
282           .type(MIMETYPE_TEXT).entity("Bad request" + CRLF)
283           .build();
284       }
285       Put put = new Put(row);
286       byte parts[][] = KeyValue.parseColumn(column);
287       if (parts.length == 2 && parts[1].length > 0) {
288         put.add(parts[0], parts[1], timestamp, message);
289       } else {
290         put.add(parts[0], null, timestamp, message);
291       }
292       table = pool.getTable(tableResource.getName());
293       table.put(put);
294       if (LOG.isDebugEnabled()) {
295         LOG.debug("PUT " + put.toString());
296       }
297       servlet.getMetrics().incrementSucessfulPutRequests(1);
298       return Response.ok().build();
299     } catch (IOException e) {
300       servlet.getMetrics().incrementFailedPutRequests(1);
301       return Response.status(Response.Status.SERVICE_UNAVAILABLE)
302         .type(MIMETYPE_TEXT).entity("Unavailable" + CRLF)
303         .build();
304     } finally {
305       if (table != null) try {
306         table.close();
307       } catch (IOException ioe) { }
308     }
309   }
310 
311   @PUT
312   @Consumes({MIMETYPE_XML, MIMETYPE_JSON, MIMETYPE_PROTOBUF,
313     MIMETYPE_PROTOBUF_IETF})
314   public Response put(final CellSetModel model,
315       final @Context UriInfo uriInfo) {
316     if (LOG.isDebugEnabled()) {
317       LOG.debug("PUT " + uriInfo.getAbsolutePath()
318         + " " + uriInfo.getQueryParameters());
319     }
320     return update(model, true);
321   }
322 
323   @PUT
324   @Consumes(MIMETYPE_BINARY)
325   public Response putBinary(final byte[] message,
326       final @Context UriInfo uriInfo, final @Context HttpHeaders headers) {
327     if (LOG.isDebugEnabled()) {
328       LOG.debug("PUT " + uriInfo.getAbsolutePath() + " as "+ MIMETYPE_BINARY);
329     }
330     return updateBinary(message, headers, true);
331   }
332 
333   @POST
334   @Consumes({MIMETYPE_XML, MIMETYPE_JSON, MIMETYPE_PROTOBUF,
335     MIMETYPE_PROTOBUF_IETF})
336   public Response post(final CellSetModel model,
337       final @Context UriInfo uriInfo) {
338     if (LOG.isDebugEnabled()) {
339       LOG.debug("POST " + uriInfo.getAbsolutePath()
340         + " " + uriInfo.getQueryParameters());
341     }
342     return update(model, false);
343   }
344 
345   @POST
346   @Consumes(MIMETYPE_BINARY)
347   public Response postBinary(final byte[] message,
348       final @Context UriInfo uriInfo, final @Context HttpHeaders headers) {
349     if (LOG.isDebugEnabled()) {
350       LOG.debug("POST " + uriInfo.getAbsolutePath() + " as "+MIMETYPE_BINARY);
351     }
352     return updateBinary(message, headers, false);
353   }
354 
355   @DELETE
356   public Response delete(final @Context UriInfo uriInfo) {
357     if (LOG.isDebugEnabled()) {
358       LOG.debug("DELETE " + uriInfo.getAbsolutePath());
359     }
360     servlet.getMetrics().incrementRequests(1);
361     if (servlet.isReadOnly()) {
362       return Response.status(Response.Status.FORBIDDEN)
363         .type(MIMETYPE_TEXT).entity("Forbidden" + CRLF)
364         .build();
365     }
366     Delete delete = null;
367     if (rowspec.hasTimestamp())
368       delete = new Delete(rowspec.getRow(), rowspec.getTimestamp());
369     else
370       delete = new Delete(rowspec.getRow());
371 
372     for (byte[] column: rowspec.getColumns()) {
373       byte[][] split = KeyValue.parseColumn(column);
374       if (rowspec.hasTimestamp()) {
375         if (split.length == 2 && split[1].length != 0) {
376           delete.deleteColumns(split[0], split[1], rowspec.getTimestamp());
377         } else {
378           delete.deleteFamily(split[0], rowspec.getTimestamp());
379         }
380       } else {
381         if (split.length == 2 && split[1].length != 0) {
382           delete.deleteColumns(split[0], split[1]);
383         } else {
384           delete.deleteFamily(split[0]);
385         }
386       }
387     }
388     HTablePool pool = servlet.getTablePool();
389     HTableInterface table = null;
390     try {
391       table = pool.getTable(tableResource.getName());
392       table.delete(delete);
393       servlet.getMetrics().incrementSucessfulDeleteRequests(1);
394       if (LOG.isDebugEnabled()) {
395         LOG.debug("DELETE " + delete.toString());
396       }
397     } catch (IOException e) {
398       servlet.getMetrics().incrementFailedDeleteRequests(1);
399       return Response.status(Response.Status.SERVICE_UNAVAILABLE)
400         .type(MIMETYPE_TEXT).entity("Unavailable" + CRLF)
401         .build();
402     } finally {
403       if (table != null) try {
404         table.close();
405       } catch (IOException ioe) { }
406     }
407     return Response.ok().build();
408   }
409 
410   /**
411    * Validates the input request parameters, parses columns from CellSetModel,
412    * and invokes checkAndPut on HTable.
413    *
414    * @param model instance of CellSetModel
415    * @return Response 200 OK, 304 Not modified, 400 Bad request
416    */
417   Response checkAndPut(final CellSetModel model) {
418     HTablePool pool = servlet.getTablePool();
419     HTableInterface table = null;
420     try {
421       if (model.getRows().size() != 1) {
422         return Response.status(Response.Status.BAD_REQUEST)
423           .type(MIMETYPE_TEXT).entity("Bad request" + CRLF)
424           .build();
425       }
426 
427       RowModel rowModel = model.getRows().get(0);
428       byte[] key = rowModel.getKey();
429       if (key == null) {
430         key = rowspec.getRow();
431       }
432 
433       List<CellModel> cellModels = rowModel.getCells();
434       int cellModelCount = cellModels.size();
435       if (key == null || cellModelCount <= 1) {
436         return Response.status(Response.Status.BAD_REQUEST)
437           .type(MIMETYPE_TEXT).entity("Bad request" + CRLF)
438           .build();
439       }
440 
441       Put put = new Put(key);
442       CellModel valueToCheckCell = cellModels.get(cellModelCount - 1);
443       byte[] valueToCheckColumn = valueToCheckCell.getColumn();
444       byte[][] valueToPutParts = KeyValue.parseColumn(valueToCheckColumn);
445       if (valueToPutParts.length == 2 && valueToPutParts[1].length > 0) {
446         CellModel valueToPutCell = null;
447         for (int i = 0, n = cellModelCount - 1; i < n ; i++) {
448           if(Bytes.equals(cellModels.get(i).getColumn(),
449               valueToCheckCell.getColumn())) {
450             valueToPutCell = cellModels.get(i);
451             break;
452           }
453         }
454         if (valueToPutCell != null) {
455           put.add(valueToPutParts[0], valueToPutParts[1], valueToPutCell
456             .getTimestamp(), valueToPutCell.getValue());
457         } else {
458           return Response.status(Response.Status.BAD_REQUEST)
459             .type(MIMETYPE_TEXT).entity("Bad request" + CRLF)
460             .build();
461         }
462       } else {
463         return Response.status(Response.Status.BAD_REQUEST)
464           .type(MIMETYPE_TEXT).entity("Bad request" + CRLF)
465           .build();
466       }
467 
468       table = pool.getTable(this.tableResource.getName());
469       boolean retValue = table.checkAndPut(key, valueToPutParts[0],
470         valueToPutParts[1], valueToCheckCell.getValue(), put);
471       if (LOG.isDebugEnabled()) {
472         LOG.debug("CHECK-AND-PUT " + put.toString() + ", returns " + retValue);
473       }
474       table.flushCommits();
475       ResponseBuilder response = Response.ok();
476       if (!retValue) {
477         response = Response.status(304);
478       }
479       return response.build();
480     } catch (IOException e) {
481       return Response.status(Response.Status.SERVICE_UNAVAILABLE)
482         .type(MIMETYPE_TEXT).entity("Unavailable" + CRLF)
483         .build();
484     } finally {
485       if (table != null) try {
486         table.close();
487       } catch (IOException ioe) { }
488     }
489   }
490 
491   /**
492    * Validates the input request parameters, parses columns from CellSetModel,
493    * and invokes checkAndDelete on HTable.
494    *
495    * @param model instance of CellSetModel
496    * @return Response 200 OK, 304 Not modified, 400 Bad request
497    */
498   Response checkAndDelete(final CellSetModel model) {
499     HTablePool pool = servlet.getTablePool();
500     HTableInterface table = null;
501     Delete delete = null;
502     try {
503       if (model.getRows().size() != 1) {
504         return Response.status(Response.Status.BAD_REQUEST)
505           .type(MIMETYPE_TEXT).entity("Bad request" + CRLF)
506           .build();
507       }
508       RowModel rowModel = model.getRows().get(0);
509       byte[] key = rowModel.getKey();
510       if (key == null) {
511         key = rowspec.getRow();
512       }
513       if (key == null) {
514         return Response.status(Response.Status.BAD_REQUEST)
515           .type(MIMETYPE_TEXT).entity("Bad request" + CRLF)
516           .build();
517       }
518 
519       delete = new Delete(key);
520       CellModel valueToDeleteCell = rowModel.getCells().get(0);
521       byte[] valueToDeleteColumn = valueToDeleteCell.getColumn();
522       if (valueToDeleteColumn == null) {
523         try {
524           valueToDeleteColumn = rowspec.getColumns()[0];
525         } catch (final ArrayIndexOutOfBoundsException e) {
526           return Response.status(Response.Status.BAD_REQUEST)
527             .type(MIMETYPE_TEXT).entity("Bad request" + CRLF)
528             .build();
529         }
530       }
531       byte[][] parts = KeyValue.parseColumn(valueToDeleteColumn);
532       if (parts.length == 2 && parts[1].length > 0) {
533         delete.deleteColumns(parts[0], parts[1]);
534       } else {
535         return Response.status(Response.Status.BAD_REQUEST)
536           .type(MIMETYPE_TEXT).entity("Bad request" + CRLF)
537           .build();
538       }
539 
540       table = pool.getTable(tableResource.getName());
541       boolean retValue = table.checkAndDelete(key, parts[0], parts[1],
542         valueToDeleteCell.getValue(), delete);
543       if (LOG.isDebugEnabled()) {
544         LOG.debug("CHECK-AND-DELETE " + delete.toString() + ", returns "
545           + retValue);
546       }
547       table.flushCommits();
548       ResponseBuilder response = Response.ok();
549       if (!retValue) {
550         response = Response.status(304);
551       }
552       return response.build();
553     } catch (IOException e) {
554       return Response.status(Response.Status.SERVICE_UNAVAILABLE)
555         .type(MIMETYPE_TEXT).entity("Unavailable" + CRLF)
556         .build();
557     } finally {
558       if (table != null) try {
559         table.close();
560       } catch (IOException ioe) {
561         LOG.debug("Exception received while closing the table", ioe);
562       }
563     }
564   }
565 }