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