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