Index: src/contrib/hbase/src/java/org/apache/hadoop/hbase/REST.java
===================================================================
--- src/contrib/hbase/src/java/org/apache/hadoop/hbase/REST.java (revision 0)
+++ src/contrib/hbase/src/java/org/apache/hadoop/hbase/REST.java (revision 0)
@@ -0,0 +1,1020 @@
+package org.apache.hadoop.hbase;
+
+
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+
+import java.io.IOException;
+import java.io.PrintWriter;
+import java.io.UnsupportedEncodingException;
+import java.net.URLDecoder;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.SortedMap;
+import java.util.TreeMap;
+
+import javax.servlet.ServletException;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+import org.apache.hadoop.hbase.util.JenkinsHash;
+import org.apache.hadoop.io.Text;
+import org.mortbay.servlet.MultiPartResponse;
+import org.znerd.xmlenc.LineBreak;
+import org.znerd.xmlenc.XMLOutputter;
+
+import javax.xml.parsers.DocumentBuilder;
+import javax.xml.parsers.DocumentBuilderFactory;
+import javax.xml.transform.Transformer;
+import javax.xml.transform.TransformerFactory;
+import javax.xml.transform.dom.DOMSource;
+import javax.xml.transform.stream.StreamResult;
+import org.w3c.dom.Document;
+import org.w3c.dom.Element;
+import org.w3c.dom.Node;
+import org.w3c.dom.NodeList;
+
+import java.util.HashSet;
+import java.util.Set;
+
+
+/**
+ * Servlet implementation class for hbase REST interface.
+ * Presumes container ensures single thread through here at any one time
+ * (Usually the default configuration). In other words, code is not
+ * written thread-safe.
+ *
This servlet has explicit dependency on Jetty server; it uses the
+ * jetty implementation of MultipartResponse.
+ *
+ *
TODO:
+ *
+ * - multipart/related response is not correct; the servlet setContentType
+ * is broken. I am unable to add parameters such as boundary or start to
+ * multipart/related. They get stripped.
+ * - Timestamp format? ISO8601? Fixed length? GMT?
+ * - Currently creating a scanner, need to specify a column. Need to make
+ * it so the HTable instance has current table's metadata to-hand so easy to
+ * find the list of all column families so can make up list of columns if none
+ * specified.
+ * - Minor items are we are decoding URLs in places where probably already
+ * done, and being able to put more than one column into a parameter, and
+ * how to timeout scanners that are in the scanner list.
+ *
+ * @see Hbase REST Specification
+ */
+public class REST extends javax.servlet.http.HttpServlet
+implements javax.servlet.Servlet {
+
+ private final Log LOG = LogFactory.getLog(this.getClass());
+
+ private static final long serialVersionUID = 6939910503474376143L;
+
+ private static final String ACCEPT = "accept";
+ private static final String ROW = "row";
+ private static final String COLUMN = "column";
+ private static final String SCANNER = "scanner";
+ private static final String TIMESTAMP = "timestamp";
+ private static final String START_ROW = "start_row";
+ private static final String END_ROW = "end_row";
+ private static final String CONTENT_TYPE = "content-type";
+
+
+ private HBaseAdmin admin;
+ private HBaseConfiguration conf;
+ private HTable table = null;
+
+ /*
+ * Map of outstanding scanners keyed by scannerid.
+ */
+ private final Map scanners =
+ new HashMap();
+
+ /*
+ * Supported content types as enums
+ */
+ private enum ContentType {
+ XML("text/xml"),
+ PLAIN("text/plain"),
+ MIME("multipart/related"),
+ NOT_ACCEPTABLE("");
+
+ private final String type;
+
+ private ContentType(final String t) {
+ this.type = t;
+ }
+
+ @Override
+ public String toString() {
+ return this.type;
+ }
+
+ /**
+ * Utility method used looking at Accept header content.
+ * @param t The content type to examine.
+ * @return The enum that matches the prefix of t or
+ * the default enum if t is empty. If unsupported type, we
+ * return NOT_ACCEPTABLE.
+ */
+ public static ContentType getContentType(final String t) {
+ // Default to text/plain. Curl sends */*.
+ if (t == null || t.equals("*/*")) {
+ return ContentType.XML;
+ }
+ String lowerCased = t.toLowerCase();
+ ContentType [] values = ContentType.values();
+ ContentType result = null;
+ for (int i = 0; i < values.length; i++) {
+ if (lowerCased.startsWith(values[i].type)) {
+ result = values[i];
+ break;
+ }
+ }
+ return result == null? NOT_ACCEPTABLE: result;
+ }
+ }
+
+ /**
+ * Default constructor
+ */
+ public REST() {
+ super();
+ }
+
+ public void init() throws ServletException {
+ super.init();
+ this.conf = new HBaseConfiguration();
+ try {
+ this.admin = new HBaseAdmin(this.conf);
+ } catch (MasterNotRunningException e) {
+ throw new ServletException(e);
+ }
+ }
+
+ protected void doGet(HttpServletRequest request, HttpServletResponse response)
+ throws IOException, ServletException {
+ String [] pathSegments = getPathSegments(request);
+ if (pathSegments.length <= 0 || pathSegments[0].length() <= 0) {
+ getTables(request, response);
+ } else {
+ getTable(request, response, pathSegments);
+ }
+ }
+
+ protected void doPost(HttpServletRequest request, HttpServletResponse response)
+ throws IOException, ServletException {
+ String [] pathSegments = getPathSegments(request);
+ if (pathSegments.length < 2 || pathSegments[0].length() <= 0) {
+ doNotFound(response, "Specify a table name to POST against");
+ } else {
+ if (pathSegments[1].toLowerCase().equals(SCANNER) &&
+ pathSegments.length >= 2) {
+ if (pathSegments.length == 2) {
+ openScanner(request, response, pathSegments);
+ } else if (pathSegments.length == 3 && pathSegments[2].length() > 0) {
+ // Increment scanner.
+ ScannerRecord sr = this.scanners.get(pathSegments[2]);
+ if (sr == null) {
+ doNotFound(response, "Scanner not found");
+ } else {
+ // If we get an exception here, close and remove scanner?
+ boolean hasNext = false;
+ try {
+ hasNext = sr.next();
+ } catch (UnknownScannerException e) {
+ // Let this show as a 404.
+ }
+ if (!hasNext) {
+ this.scanners.remove(pathSegments[2]);
+ sr.getScanner().close();
+ doNotFound(response, "Scanner exhausted");
+ } else {
+ getScanner(request, response, pathSegments[2]);
+ }
+ }
+ } else {
+ doNotFound(response);
+ }
+ } else if (pathSegments[1].toLowerCase().equals(ROW) && pathSegments.length >= 3) {
+ focusTable(getTableName(pathSegments));
+ putRow(request, response, pathSegments);
+ } else {
+ doNotFound(response, "No handler for " + request.getPathInfo());
+ }
+ }
+ }
+
+
+ protected void doPut(HttpServletRequest request, HttpServletResponse response)
+ throws ServletException, IOException {
+ // Equate PUT with a POST.
+ doPost(request, response);
+ }
+
+ protected void doDelete(HttpServletRequest request,
+ HttpServletResponse response)
+ throws IOException, ServletException {
+ String [] pathSegments = getPathSegments(request);
+ if (pathSegments.length < 2 || pathSegments[0].length() <= 0) {
+ doNotFound(response, "Specify a table name to DELETE");
+ } else {
+ if (pathSegments[1].toLowerCase().equals(SCANNER) &&
+ pathSegments.length == 3 && pathSegments[2].length() > 0) {
+ deleteScanner(response, pathSegments[2]);
+ } else if (pathSegments[1].toLowerCase().equals(ROW) &&
+ pathSegments.length >= 3) {
+ deleteRow(request, response, pathSegments);
+ } else {
+ doNotFound(response, "No handler");
+ }
+ }
+ }
+
+ private void deleteRow(final HttpServletRequest request,
+ final HttpServletResponse response, final String [] pathSegments)
+ throws IOException, ServletException {
+ // grab the table we're operating on
+ focusTable(getTableName(pathSegments));
+
+ Text key = new Text(pathSegments[2]);
+
+ String[] columns = request.getParameterValues(COLUMN);
+
+ // hack - we'll actually test for the presence of the timestamp parameter
+ // eventually
+ boolean timestamp_present = false;
+ if(timestamp_present){ // do a timestamp-aware delete
+ doMethodNotAllowed(response, "DELETE with a timestamp not implemented!");
+ }
+ else{ // ignore timestamps
+ if(columns == null || columns.length == 0){
+ // retrieve all the columns
+ doMethodNotAllowed(response, "DELETE without specified columns not implemented!");
+ }
+ else{
+ // delete each column in turn
+ for(int i = 0; i < columns.length; i++){
+ this.table.deleteAll(key, new Text(columns[i]));
+ }
+ }
+
+ response.setStatus(202);
+ }
+ }
+
+ /*
+ * Data structure to hold all related to a particular scanner -- its
+ * implementation and the current key and value.
+ */
+ private class ScannerRecord {
+ private final HScannerInterface scanner;
+ private HStoreKey key = null;
+ private SortedMap value = null;
+
+ ScannerRecord(final HScannerInterface s) {
+ this.scanner = s;
+ }
+
+ public HScannerInterface getScanner() {
+ return this.scanner;
+ }
+
+ public HStoreKey getKey() {
+ return this.key;
+ }
+
+ public SortedMap getValue() {
+ return this.value;
+ }
+
+ /**
+ * Call next on the scanner.
+ * @return True if more values in scanner.
+ * @throws IOException
+ */
+ public boolean next() throws IOException {
+ this.key = new HStoreKey();
+ this.value = new TreeMap();
+ return this.scanner.next(this.key, this.value);
+ }
+ }
+
+ /*
+ * Create scanner
+ * @param request
+ * @param response
+ * @param pathSegments
+ * @throws IOException
+ */
+ private void openScanner(final HttpServletRequest request,
+ final HttpServletResponse response, final String [] pathSegments)
+ throws IOException, ServletException {
+ // focus on the table
+ focusTable(getTableName(pathSegments));
+
+ // get the list of columns we're supposed to interact with
+ String[] raw_columns = request.getParameterValues(COLUMN);
+ Text [] columns = null;
+
+ if (raw_columns != null) {
+ columns = new Text [raw_columns.length];
+ for (int i = 0; i < raw_columns.length; i++) {
+ // I think this decoding is redundant.
+ columns[i] =
+ new Text(URLDecoder.decode(raw_columns[i], HConstants.UTF8_ENCODING));
+ }
+ } else {
+ // TODO: Need to put into the scanner all of the table's column
+ // families. TODO: Verify this returns all rows. For now just fail.
+ doMethodNotAllowed(response, "Unspecified columns parameter currently not supported!");
+ return;
+ }
+
+ // TODO: Parse according to the timestamp format we agree on.
+ // TODO: Are these decodings redundant?
+ String raw_ts = request.getParameter(TIMESTAMP);
+ Text startRow = request.getParameter(START_ROW) == null?
+ HConstants.EMPTY_START_ROW:
+ new Text(URLDecoder.decode(request.getParameter(START_ROW),
+ HConstants.UTF8_ENCODING));
+ // Empty start row is same value as empty end row.
+ Text endRow = request.getParameter(END_ROW) == null?
+ HConstants.EMPTY_START_ROW:
+ new Text(URLDecoder.decode(request.getParameter(END_ROW),
+ HConstants.UTF8_ENCODING));
+ HScannerInterface scanner = (request.getParameter(END_ROW) == null)?
+ this.table.obtainScanner(columns, startRow):
+ this.table.obtainScanner(columns, startRow, endRow);
+ // Make a scanner id by hashing the object toString value (object name +
+ // an id). Will make identifier less burdensome and more url friendly.
+ String scannerid =
+ Integer.toHexString(JenkinsHash.hash(scanner.toString().getBytes(), -1));
+ ScannerRecord sr = new ScannerRecord(scanner);
+ this.scanners.put(scannerid, sr);
+ response.setStatus(201);
+ response.addHeader("Location", request.getContextPath() + "/" +
+ pathSegments[0] + "/" + pathSegments[1] + "/" + scannerid);
+ response.getOutputStream().close();
+ }
+
+ /*
+ * Delete scanner
+ * @param response
+ * @param scannerid
+ * @throws IOException
+ */
+ private void deleteScanner(final HttpServletResponse response,
+ final String scannerid)
+ throws IOException, ServletException {
+ ScannerRecord sr = this.scanners.remove(scannerid);
+ if (sr == null) {
+ doNotFound(response, "No such scanner");
+ } else {
+ sr.getScanner().close();
+ response.setStatus(200);
+ response.getOutputStream().close();
+ }
+ }
+ /*
+ * @param request
+ * @return request pathinfo split on the '/' ignoring the first '/' so first
+ * element in pathSegment is not the empty string.
+ */
+ private String [] getPathSegments(final HttpServletRequest request) {
+ return request.getPathInfo().substring(1).split("/");
+ }
+
+ /*
+ * Set content-type, encoding, and status on passed response
+ * @param response
+ * @param status
+ * @param contentType
+ */
+ private void setResponseHeader(final HttpServletResponse response,
+ final int status, final String contentType) {
+ // Container adds the charset to the HTTP content-type header.
+ response.setContentType(contentType);
+ response.setCharacterEncoding(HConstants.UTF8_ENCODING);
+ response.setStatus(status);
+ }
+
+ /*
+ * If we can't do the specified Accepts header type.
+ * @param response
+ * @throws IOException
+ */
+ private void doNotAcceptable(final HttpServletResponse response)
+ throws IOException {
+ response.sendError(HttpServletResponse.SC_NOT_ACCEPTABLE);
+ }
+
+ /*
+ * Resource not found.
+ * @param response
+ * @throws IOException
+ */
+ private void doNotFound(final HttpServletResponse response)
+ throws IOException {
+ response.sendError(HttpServletResponse.SC_NOT_FOUND);
+ }
+
+ /*
+ * Resource not found.
+ * @param response
+ * @param msg
+ * @throws IOException
+ */
+ private void doNotFound(final HttpServletResponse response, final String msg)
+ throws IOException {
+ response.sendError(HttpServletResponse.SC_NOT_FOUND, msg);
+ }
+
+ /*
+ * Unimplemented method.
+ * @param response
+ * @param message to send
+ * @throws IOException
+ */
+ private void doMethodNotAllowed(final HttpServletResponse response,
+ final String message)
+ throws IOException {
+ response.sendError(HttpServletResponse.SC_METHOD_NOT_ALLOWED, message);
+ }
+
+ /*
+ * @param o
+ * @return XMLOutputter wrapped around o.
+ * @throws IllegalStateException
+ * @throws IOException
+ */
+ private XMLOutputter getXMLOutputter(final PrintWriter o)
+ throws IllegalStateException, IOException {
+ XMLOutputter outputter = new XMLOutputter(o, HConstants.UTF8_ENCODING);
+ outputter.setLineBreak(LineBreak.UNIX);
+ outputter.setIndentation(" ");
+ outputter.declaration();
+ return outputter;
+ }
+
+ /*
+ * Write an XML element.
+ * @param outputter
+ * @param name
+ * @param value
+ * @throws IllegalStateException
+ * @throws IOException
+ */
+ private void doElement(final XMLOutputter outputter,
+ final String name, final String value)
+ throws IllegalStateException, IOException {
+ outputter.startTag(name);
+ if (value.length() > 0) {
+ outputter.pcdata(value);
+ }
+ outputter.endTag();
+ }
+
+ private String getTableName(final String [] pathSegments)
+ throws UnsupportedEncodingException {
+ // Get table name? First part of passed segment. It can't be empty string
+ // or null because we should have tested for that before coming in here.
+ return URLDecoder.decode(pathSegments[0], HConstants.UTF8_ENCODING);
+ }
+
+ private void focusTable(final String tableName) throws IOException {
+ // Do we have an HTable instance to suit? TODO, keep a pool of
+ // instances of HTable. For now, allocate a new one each time table
+ // focus changes.
+ if (this.table == null ||
+ !this.table.getTableName().toString().equals(tableName)) {
+ if (this.table != null) {
+ this.table.close();
+ }
+ this.table = new HTable(this.conf, new Text(tableName));
+ }
+ }
+
+ /*
+ * @param tableName
+ * @return True if tableName exists.
+ * @throws IOException
+ */
+ private boolean tableExists(final String tableName) throws IOException {
+ boolean result = false;
+ HTableDescriptor [] tables = this.admin.listTables();
+ for (int i = 0; i < tables.length; i++) {
+ if (tables[i].getName().toString().equals(tableName)) {
+ result = true;
+ break;
+ }
+ }
+ return result;
+ }
+
+ /*
+ * Return list of tables.
+ * @param request
+ * @param response
+ */
+ private void getTables(final HttpServletRequest request,
+ final HttpServletResponse response)
+ throws IOException {
+ HTableDescriptor [] tables = this.admin.listTables();
+ switch (ContentType.getContentType(request.getHeader(ACCEPT))) {
+ case XML:
+ setResponseHeader(response, tables.length > 0? 200: 204,
+ ContentType.XML.toString());
+ XMLOutputter outputter = getXMLOutputter(response.getWriter());
+ outputter.startTag("tables");
+ for (int i = 0; i < tables.length; i++) {
+ doElement(outputter, "table", tables[i].getName().toString());
+ }
+ outputter.endTag();
+ outputter.endDocument();
+ outputter.getWriter().close();
+ break;
+ case PLAIN:
+ setResponseHeader(response, tables.length > 0? 200: 204,
+ ContentType.PLAIN.toString());
+ PrintWriter out = response.getWriter();
+ for (int i = 0; i < tables.length; i++) {
+ out.println(tables[i].getName().toString());
+ }
+ out.close();
+ break;
+ default:
+ doNotAcceptable(response);
+ }
+ }
+
+ /*
+ * Do GET against passed table name.
+ * @param request
+ * @param response
+ * @param Array of path elements split on the '/' character. First segment
+ * has the table name in it.
+ * @throws IOException
+ */
+ private void getTable(final HttpServletRequest request,
+ final HttpServletResponse response, final String [] pathSegments)
+ throws IOException {
+ String tableName = getTableName(pathSegments);
+ if (!tableExists(tableName)) {
+ doNotFound(response, "Table does not exist");
+ return;
+ }
+ if (pathSegments.length == 1) {
+ // If only a table name specified, print out table metadata.
+ getTableMetadata(request, response, tableName);
+ } else {
+ // Return a row or regions or scanner values for specified table.
+ focusTable(tableName);
+ // What does user want to do against this table?
+ String resource = pathSegments[1].toLowerCase();
+ if (pathSegments.length == 2 && resource.equals("regions")) {
+ getTableRegions(request, response);
+ } else if (pathSegments.length >= 3 && resource.equals(ROW)) {
+ getRow(request, response, pathSegments);
+ } else if (pathSegments.length == 3 && resource.equals(SCANNER)) {
+ getScanner(request, response, pathSegments[2]);
+ } else {
+ doNotFound(response, "No handler");
+ }
+ }
+ }
+
+ /*
+ * Return element at current scanner position.
+ * @param request
+ * @param response
+ * @param scannerid
+ * @throws IOException
+ */
+ private void getScanner(final HttpServletRequest request,
+ final HttpServletResponse response, final String scannerid)
+ throws IOException {
+ ScannerRecord sr = this.scanners.get(scannerid);
+ if (sr == null) {
+ doNotFound(response, "Current key/value for scanner not set");
+ return;
+ }
+ switch (ContentType.getContentType(request.getHeader(ACCEPT))) {
+ case XML:
+ setResponseHeader(response, 200, ContentType.XML.toString());
+ XMLOutputter outputter = getXMLOutputter(response.getWriter());
+ outputter.startTag(ROW);
+ doElement(outputter, "name", sr.getKey().getRow().toString());
+ // Normally no column is supplied when scanning.
+ if (sr.getKey().getColumn() != null &&
+ sr.getKey().getColumn().getLength() > 0) {
+ doElement(outputter, "key-column", sr.getKey().getColumn().toString());
+ }
+ doElement(outputter, "timestamp",
+ Long.toString(sr.getKey().getTimestamp()));
+ outputColumnsXml(outputter, sr.getValue());
+ outputter.endTag();
+ outputter.endDocument();
+ outputter.getWriter().close();
+ break;
+ case MIME:
+ outputScannerEntryMime(response, sr);
+ break;
+ default:
+ doNotAcceptable(response);
+ }
+ }
+
+ private void outputScannerEntryMime(final HttpServletResponse response,
+ final ScannerRecord sr)
+ throws IOException {
+ response.setStatus(200);
+ // This code ties me to the jetty server.
+ MultiPartResponse mpr = new MultiPartResponse(response);
+ // Content type should look like this for multipart:
+ // Content-type: multipart/related;start="";type="application/xop+xml";boundary="uuid:94ebf1e6-7eb5-43f1-85f4-2615fc40c5d6";start-info="text/xml"
+ String ct = ContentType.MIME.toString() + ";charset=\"UTF-8\";boundary=\"" +
+ mpr.getBoundary() + "\"";
+ // Setting content type is broken. I'm unable to set parameters on the
+ // content-type; They get stripped. Can't set boundary, etc.
+ // response.addHeader("Content-Type", ct);
+ response.setContentType(ct);
+ // Write row, key-column and timestamp each in its own part.
+ mpr.startPart("application/octet-stream",
+ new String [] {"Content-Description: row",
+ "Content-Transfer-Encoding: binary",
+ "Content-Length: " + sr.getKey().getRow().getBytes().length});
+ mpr.getOut().write(sr.getKey().getRow().getBytes());
+
+ // Usually key-column is empty when scanning.
+ if (sr.getKey().getColumn() != null &&
+ sr.getKey().getColumn().getLength() > 0) {
+ mpr.startPart("application/octet-stream",
+ new String [] {"Content-Description: key-column",
+ "Content-Transfer-Encoding: binary",
+ "Content-Length: " + sr.getKey().getColumn().getBytes().length});
+ }
+ mpr.getOut().write(sr.getKey().getColumn().getBytes());
+ // TODO: Fix. Need to write out the timestamp in the ordained timestamp
+ // format.
+ byte [] timestampBytes = Long.toString(sr.getKey().getTimestamp()).getBytes();
+ mpr.startPart("application/octet-stream",
+ new String [] {"Content-Description: timestamp",
+ "Content-Transfer-Encoding: binary",
+ "Content-Length: " + timestampBytes.length});
+ mpr.getOut().write(timestampBytes);
+ // Write out columns
+ outputColumnsMime(mpr, sr.getValue());
+ mpr.close();
+ }
+
+ /*
+ * @param request
+ * @param response
+ * @param pathSegments info path split on the '/' character. First segment
+ * is the tablename, second is 'row', and third is the row id.
+ * @throws IOException
+ */
+ private void getRow(final HttpServletRequest request,
+ final HttpServletResponse response, final String [] pathSegments)
+ throws IOException {
+ // pull the row key out of the path
+ String row = URLDecoder.decode(pathSegments[2], HConstants.UTF8_ENCODING);
+
+ String timestampStr = null;
+ if (pathSegments.length == 4) {
+ // A timestamp has been supplied.
+ timestampStr = pathSegments[3];
+ if (timestampStr.equals("timestamps")) {
+ // Not supported in hbase just yet. TODO
+ doMethodNotAllowed(response, "Not yet supported by hbase");
+ return;
+ }
+ }
+
+ String[] columns = request.getParameterValues(COLUMN);
+
+ if (columns == null || columns.length == 0) {
+ // They want full row returned.
+
+ // Presumption is that this.table has already been focused on target table.
+ Map result = timestampStr == null ?
+ this.table.getRow(new Text(row))
+ : this.table.getRow(new Text(row), Long.parseLong(timestampStr));
+
+ if (result == null || result.size() == 0) {
+ doNotFound(response);
+ } else {
+ switch (ContentType.getContentType(request.getHeader(ACCEPT))) {
+ case XML:
+ outputRowXml(response, result);
+ break;
+ case MIME:
+ outputRowMime(response, result);
+ break;
+ default:
+ doNotAcceptable(response);
+ }
+ }
+ } else {
+ // One or more columns specified though only one supported in hbase
+ // currently. Says in spec that can be a list of semi-colon delimited
+ // columns but semi-colon could be part of a column name.
+ //column = URLDecoder.decode(column, HConstants.UTF8_ENCODING);
+
+ // Presumption is that this.table has already been set against target table
+ Map prefiltered_result = this.table.getRow(new Text(row));
+
+ if (prefiltered_result == null || prefiltered_result.size() == 0) {
+ doNotFound(response);
+ } else {
+ // create a Set from the columns requested so we can
+ // efficiently filter
+ Set requested_columns_set = new HashSet();
+ for(int i = 0; i < columns.length; i++){
+ requested_columns_set.add(columns[i]);
+ }
+
+ // output map that will contain the filtered results
+ Map m = new HashMap();
+
+ // get an array of all the columns retrieved
+ Object[] columns_retrieved = prefiltered_result.keySet().toArray();
+
+ // copy over those cells with requested column names
+ for(int i = 0; i < columns_retrieved.length; i++){
+ Text current_column = (Text)columns_retrieved[i];
+ if(requested_columns_set.contains(current_column.toString())){
+ m.put(current_column, prefiltered_result.get(current_column));
+ }
+ }
+
+ switch (ContentType.getContentType(request.getHeader(ACCEPT))) {
+ case XML:
+ outputRowXml(response, m);
+ break;
+ case MIME:
+ outputRowMime(response, m);
+ break;
+ default:
+ doNotAcceptable(response);
+ }
+ }
+ }
+ }
+
+ private void putRow(final HttpServletRequest request,
+ final HttpServletResponse response, final String [] pathSegments)
+ throws IOException, ServletException {
+ switch(ContentType.getContentType(request.getHeader(CONTENT_TYPE))) {
+ case XML:
+ putRowXml(request, response, pathSegments);
+ break;
+ case MIME:
+ doNotAcceptable(response);
+ break;
+ default:
+ doNotAcceptable(response);
+ }
+ }
+
+ private void putRowXml(final HttpServletRequest request,
+ final HttpServletResponse response, final String [] pathSegments)
+ throws IOException, ServletException{
+
+ DocumentBuilderFactory docBuilderFactory
+ = DocumentBuilderFactory.newInstance();
+ //ignore all comments inside the xml file
+ docBuilderFactory.setIgnoringComments(true);
+
+ DocumentBuilder builder = null;
+ Document doc = null;
+
+ String timestamp = pathSegments.length >= 4 ? pathSegments[3] : null;
+
+ try{
+ builder = docBuilderFactory.newDocumentBuilder();
+ doc = builder.parse(request.getInputStream());
+ } catch (javax.xml.parsers.ParserConfigurationException e) {
+ throw new ServletException(e);
+ } catch (org.xml.sax.SAXException e){
+ throw new ServletException(e);
+ }
+
+ long lock_id = -1;
+
+ try{
+ // start an update
+ Text key = new Text(pathSegments[2]);
+ lock_id = this.table.startUpdate(key);
+
+ // set the columns from the xml
+ NodeList columns = doc.getElementsByTagName("column");
+
+ for(int i = 0; i < columns.getLength(); i++){
+ // get the current column element we're working on
+ Element column = (Element)columns.item(i);
+
+ // extract the name and value children
+ Node name_node = column.getElementsByTagName("name").item(0);
+ Text name = new Text(name_node.getFirstChild().getNodeValue());
+
+ Node value_node = column.getElementsByTagName("value").item(0);
+
+ // decode the base64'd value
+ byte[] value = org.apache.hadoop.hbase.util.Base64.decode(value_node.getFirstChild().getNodeValue());
+
+ // put the value
+ this.table.put(lock_id, name, value);
+ }
+
+ // commit the update
+ if (timestamp != null) {
+ this.table.commit(lock_id, Long.parseLong(timestamp));
+ }
+ else{
+ this.table.commit(lock_id);
+ }
+
+ // respond with a 200
+ response.setStatus(200);
+ }
+ catch(Exception e){
+ if (lock_id != -1) {
+ this.table.abort(lock_id);
+ }
+ throw new ServletException(e);
+ }
+ }
+
+ /*
+ * Output row columns
+ * @param outputter
+ * @param m
+ * @throws IllegalStateException
+ * @throws IllegalArgumentException
+ * @throws IOException
+ */
+ private void outputColumnsXml(final XMLOutputter outputter,
+ final Map m)
+ throws IllegalStateException, IllegalArgumentException, IOException {
+ for (Map.Entry e: m.entrySet()) {
+ outputter.startTag(COLUMN);
+ doElement(outputter, "name", e.getKey().toString());
+ // We don't know String from binary data so we always base64 encode.
+ doElement(outputter, "value",
+ org.apache.hadoop.hbase.util.Base64.encodeBytes(e.getValue()));
+ outputter.endTag();
+ }
+ }
+
+ /*
+ * Output a row.
+ * @param response
+ * @param result
+ * @throws IOException
+ */
+ private void outputRowXml(final HttpServletResponse response,
+ final Map result)
+ throws IOException {
+ setResponseHeader(response, result.size() > 0? 200: 204,
+ ContentType.XML.toString());
+ XMLOutputter outputter = getXMLOutputter(response.getWriter());
+ outputter.startTag(ROW);
+ outputColumnsXml(outputter, result);
+ outputter.endTag();
+ outputter.endDocument();
+ outputter.getWriter().close();
+ }
+
+ private void outputColumnsMime(final MultiPartResponse mpr,
+ final Map m)
+ throws IOException {
+ for (Map.Entry e: m.entrySet()) {
+ mpr.startPart("application/octet-stream",
+ new String [] {"Content-Description: " + e.getKey().toString(),
+ "Content-Transfer-Encoding: binary",
+ "Content-Length: " + e.getValue().length});
+ mpr.getOut().write(e.getValue());
+ }
+ }
+
+ private void outputRowMime(final HttpServletResponse response,
+ final Map result)
+ throws IOException {
+ response.setStatus(result.size() > 0? 200: 204);
+ // This code ties me to the jetty server.
+ MultiPartResponse mpr = new MultiPartResponse(response);
+ // Content type should look like this for multipart:
+ // Content-type: multipart/related;start="";type="application/xop+xml";boundary="uuid:94ebf1e6-7eb5-43f1-85f4-2615fc40c5d6";start-info="text/xml"
+ String ct = ContentType.MIME.toString() + ";charset=\"UTF-8\";boundary=\"" +
+ mpr.getBoundary() + "\"";
+ // Setting content type is broken. I'm unable to set parameters on the
+ // content-type; They get stripped. Can't set boundary, etc.
+ // response.addHeader("Content-Type", ct);
+ response.setContentType(ct);
+ outputColumnsMime(mpr, result);
+ mpr.close();
+ }
+
+ /*
+ * Return region offsets.
+ * @param request
+ * @param response
+ */
+ private void getTableRegions(final HttpServletRequest request,
+ final HttpServletResponse response)
+ throws IOException {
+ // Presumption is that this.table has already been focused on target table.
+ Text [] startKeys = this.table.getStartKeys();
+ // Presumption is that this.table has already been set against target table
+ switch (ContentType.getContentType(request.getHeader(ACCEPT))) {
+ case XML:
+ setResponseHeader(response, startKeys.length > 0? 200: 204,
+ ContentType.XML.toString());
+ XMLOutputter outputter = getXMLOutputter(response.getWriter());
+ outputter.startTag("regions");
+ for (int i = 0; i < startKeys.length; i++) {
+ doElement(outputter, "region", startKeys[i].toString());
+ }
+ outputter.endTag();
+ outputter.endDocument();
+ outputter.getWriter().close();
+ break;
+ case PLAIN:
+ setResponseHeader(response, startKeys.length > 0? 200: 204,
+ ContentType.PLAIN.toString());
+ PrintWriter out = response.getWriter();
+ for (int i = 0; i < startKeys.length; i++) {
+ // TODO: Add in the server location. Is it needed?
+ out.print(startKeys[i].toString());
+ }
+ out.close();
+ break;
+ case MIME:
+ default:
+ doNotAcceptable(response);
+ }
+ }
+
+ /*
+ * Get table metadata.
+ * @param request
+ * @param response
+ * @param tableName
+ * @throws IOException
+ */
+ private void getTableMetadata(final HttpServletRequest request,
+ final HttpServletResponse response, final String tableName)
+ throws IOException {
+ HTableDescriptor [] tables = this.admin.listTables();
+ HTableDescriptor descriptor = null;
+ for (int i = 0; i < tables.length; i++) {
+ if (tables[i].getName().toString().equals(tableName)) {
+ descriptor = tables[i];
+ break;
+ }
+ }
+ if (descriptor == null) {
+ doNotFound(response);
+ } else {
+ // Presumption is that this.table has already been set against target table
+ ContentType type = ContentType.getContentType(request.getHeader(ACCEPT));
+ switch (type) {
+ case XML:
+ setResponseHeader(response, 200, ContentType.XML.toString());
+ XMLOutputter outputter = getXMLOutputter(response.getWriter());
+ outputter.startTag("table");
+ doElement(outputter, "name", descriptor.getName().toString());
+ outputter.startTag("columnfamilies");
+ for (Map.Entry e:
+ descriptor.getFamilies().entrySet()) {
+ outputter.startTag("columnfamily");
+ doElement(outputter, "name", e.getKey().toString());
+ HColumnDescriptor hcd = e.getValue();
+ doElement(outputter, "compression", hcd.getCompression().toString());
+ doElement(outputter, "bloomfilter",
+ hcd.getBloomFilter() == null? "NONE": hcd.getBloomFilter().toString());
+ doElement(outputter, "max-versions",
+ Integer.toString(hcd.getMaxVersions()));
+ doElement(outputter, "maximum-cell-size",
+ Integer.toString(hcd.getMaxValueLength()));
+ outputter.endTag();
+ }
+ outputter.endTag();
+ outputter.endTag();
+ outputter.endDocument();
+ outputter.getWriter().close();
+ break;
+ case PLAIN:
+ setResponseHeader(response, 200, ContentType.PLAIN.toString());
+ PrintWriter out = response.getWriter();
+ out.print(descriptor.toString());
+ out.close();
+ break;
+ case MIME:
+ default:
+ doNotAcceptable(response);
+ }
+ }
+ }
+}
\ No newline at end of file
Index: src/contrib/hbase/src/java/org/apache/hadoop/hbase/util/InfoServer.java
===================================================================
--- src/contrib/hbase/src/java/org/apache/hadoop/hbase/util/InfoServer.java (revision 598720)
+++ src/contrib/hbase/src/java/org/apache/hadoop/hbase/util/InfoServer.java (working copy)
@@ -89,17 +89,26 @@
this.webServer.addContext(staticContext);
// set up the context for "/" jsp files
+ String webappDir = getWebAppDir(name);
+ this.webAppContext =
+ this.webServer.addWebApplication("/", webappDir);
+ if (name.equals("master")) {
+ // Put up the rest webapp.
+ this.webServer.addWebApplication("/api", getWebAppDir("rest"));
+ }
+ addServlet("stacks", "/stacks", StatusHttpServer.StackServlet.class);
+ addServlet("logLevel", "/logLevel", org.apache.hadoop.log.LogLevel.Servlet.class);
+ }
+
+ private String getWebAppDir(final String webappName) throws IOException {
String webappDir = null;
try {
- webappDir = getWebAppsPath("webapps" + File.separator + name);
+ webappDir = getWebAppsPath("webapps" + File.separator + webappName);
} catch (FileNotFoundException e) {
// Retry. Resource may be inside jar on a windows machine.
- webappDir = getWebAppsPath("webapps/" + name);
+ webappDir = getWebAppsPath("webapps/" + webappName);
}
- this.webAppContext =
- this.webServer.addWebApplication("/", webappDir);
- addServlet("stacks", "/stacks", StatusHttpServer.StackServlet.class);
- addServlet("logLevel", "/logLevel", org.apache.hadoop.log.LogLevel.Servlet.class);
+ return webappDir;
}
/**