001/*
002 * Licensed to the Apache Software Foundation (ASF) under one
003 * or more contributor license agreements.  See the NOTICE file
004 * distributed with this work for additional information
005 * regarding copyright ownership.  The ASF licenses this file
006 * to you under the Apache License, Version 2.0 (the
007 * "License"); you may not use this file except in compliance
008 * with the License.  You may obtain a copy of the License at
009 *
010 *     http://www.apache.org/licenses/LICENSE-2.0
011 *
012 * Unless required by applicable law or agreed to in writing, software
013 * distributed under the License is distributed on an "AS IS" BASIS,
014 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
015 * See the License for the specific language governing permissions and
016 * limitations under the License.
017 */
018package org.apache.hadoop.hbase.rest;
019
020import java.io.IOException;
021import java.util.List;
022import javax.servlet.ServletContext;
023import org.apache.hadoop.hbase.NamespaceDescriptor;
024import org.apache.hadoop.hbase.client.Admin;
025import org.apache.hadoop.hbase.client.TableDescriptor;
026import org.apache.hadoop.hbase.rest.model.NamespacesInstanceModel;
027import org.apache.hadoop.hbase.rest.model.TableListModel;
028import org.apache.hadoop.hbase.rest.model.TableModel;
029import org.apache.hadoop.hbase.util.Bytes;
030import org.apache.yetus.audience.InterfaceAudience;
031import org.slf4j.Logger;
032import org.slf4j.LoggerFactory;
033
034import org.apache.hbase.thirdparty.javax.ws.rs.Consumes;
035import org.apache.hbase.thirdparty.javax.ws.rs.DELETE;
036import org.apache.hbase.thirdparty.javax.ws.rs.GET;
037import org.apache.hbase.thirdparty.javax.ws.rs.POST;
038import org.apache.hbase.thirdparty.javax.ws.rs.PUT;
039import org.apache.hbase.thirdparty.javax.ws.rs.Path;
040import org.apache.hbase.thirdparty.javax.ws.rs.PathParam;
041import org.apache.hbase.thirdparty.javax.ws.rs.Produces;
042import org.apache.hbase.thirdparty.javax.ws.rs.core.Context;
043import org.apache.hbase.thirdparty.javax.ws.rs.core.HttpHeaders;
044import org.apache.hbase.thirdparty.javax.ws.rs.core.Response;
045import org.apache.hbase.thirdparty.javax.ws.rs.core.UriInfo;
046
047/**
048 * Implements the following REST end points:
049 * <p>
050 * <tt>/namespaces/{namespace} GET: get namespace properties.</tt>
051 * <tt>/namespaces/{namespace} POST: create namespace.</tt>
052 * <tt>/namespaces/{namespace} PUT: alter namespace.</tt>
053 * <tt>/namespaces/{namespace} DELETE: drop namespace.</tt>
054 * <tt>/namespaces/{namespace}/tables GET: list namespace's tables.</tt>
055 * <p>
056 */
057@InterfaceAudience.Private
058public class NamespacesInstanceResource extends ResourceBase {
059
060  private static final Logger LOG = LoggerFactory.getLogger(NamespacesInstanceResource.class);
061  String namespace;
062  boolean queryTables = false;
063
064  /**
065   * Constructor for standard NamespaceInstanceResource.
066   */
067  public NamespacesInstanceResource(String namespace) throws IOException {
068    this(namespace, false);
069  }
070
071  /**
072   * Constructor for querying namespace table list via NamespaceInstanceResource.
073   */
074  public NamespacesInstanceResource(String namespace, boolean queryTables) throws IOException {
075    super();
076    this.namespace = namespace;
077    this.queryTables = queryTables;
078  }
079
080  /**
081   * Build a response for GET namespace description or GET list of namespace tables.
082   * @param context servlet context
083   * @param uriInfo (JAX-RS context variable) request URL
084   * @return A response containing NamespacesInstanceModel for a namespace descriptions and
085   *         TableListModel for a list of namespace tables.
086   */
087  @GET
088  @Produces({ MIMETYPE_TEXT, MIMETYPE_XML, MIMETYPE_JSON, MIMETYPE_PROTOBUF,
089    MIMETYPE_PROTOBUF_IETF })
090  public Response get(final @Context ServletContext context, final @Context UriInfo uriInfo) {
091    if (LOG.isTraceEnabled()) {
092      LOG.trace("GET " + uriInfo.getAbsolutePath());
093    }
094    servlet.getMetrics().incrementRequests(1);
095
096    // Respond to list of namespace tables requests.
097    if (queryTables) {
098      TableListModel tableModel = new TableListModel();
099      try {
100        List<TableDescriptor> tables =
101          servlet.getAdmin().listTableDescriptorsByNamespace(Bytes.toBytes(namespace));
102        for (TableDescriptor table : tables) {
103          tableModel.add(new TableModel(table.getTableName().getQualifierAsString()));
104        }
105
106        servlet.getMetrics().incrementSucessfulGetRequests(1);
107        return Response.ok(tableModel).build();
108      } catch (IOException e) {
109        servlet.getMetrics().incrementFailedGetRequests(1);
110        throw new RuntimeException("Cannot retrieve table list for '" + namespace + "'.");
111      }
112    }
113
114    // Respond to namespace description requests.
115    try {
116      NamespacesInstanceModel rowModel = new NamespacesInstanceModel(servlet.getAdmin(), namespace);
117      servlet.getMetrics().incrementSucessfulGetRequests(1);
118      return Response.ok(rowModel).build();
119    } catch (IOException e) {
120      servlet.getMetrics().incrementFailedGetRequests(1);
121      throw new RuntimeException("Cannot retrieve info for '" + namespace + "'.");
122    }
123  }
124
125  /**
126   * Build a response for PUT alter namespace with properties specified.
127   * @param model   properties used for alter.
128   * @param uriInfo (JAX-RS context variable) request URL
129   * @return response code.
130   */
131  @PUT
132  @Consumes({ MIMETYPE_XML, MIMETYPE_JSON, MIMETYPE_PROTOBUF, MIMETYPE_PROTOBUF_IETF })
133  public Response put(final NamespacesInstanceModel model, final @Context UriInfo uriInfo) {
134    return processUpdate(model, true, uriInfo);
135  }
136
137  /**
138   * Build a response for POST create namespace with properties specified.
139   * @param model   properties used for create.
140   * @param uriInfo (JAX-RS context variable) request URL
141   * @return response code.
142   */
143  @POST
144  @Consumes({ MIMETYPE_XML, MIMETYPE_JSON, MIMETYPE_PROTOBUF, MIMETYPE_PROTOBUF_IETF })
145  public Response post(final NamespacesInstanceModel model, final @Context UriInfo uriInfo) {
146    return processUpdate(model, false, uriInfo);
147  }
148
149  // Check that POST or PUT is valid and then update namespace.
150  private Response processUpdate(NamespacesInstanceModel model, final boolean updateExisting,
151    final UriInfo uriInfo) {
152    if (LOG.isTraceEnabled()) {
153      LOG.trace((updateExisting ? "PUT " : "POST ") + uriInfo.getAbsolutePath());
154    }
155    if (model == null) {
156      try {
157        model = new NamespacesInstanceModel(namespace);
158      } catch (IOException ioe) {
159        servlet.getMetrics().incrementFailedPutRequests(1);
160        throw new RuntimeException("Cannot retrieve info for '" + namespace + "'.");
161      }
162    }
163    servlet.getMetrics().incrementRequests(1);
164
165    if (servlet.isReadOnly()) {
166      servlet.getMetrics().incrementFailedPutRequests(1);
167      return Response.status(Response.Status.FORBIDDEN).type(MIMETYPE_TEXT)
168        .entity("Forbidden" + CRLF).build();
169    }
170
171    Admin admin = null;
172    boolean namespaceExists = false;
173    try {
174      admin = servlet.getAdmin();
175      namespaceExists = doesNamespaceExist(admin, namespace);
176    } catch (IOException e) {
177      servlet.getMetrics().incrementFailedPutRequests(1);
178      return processException(e);
179    }
180
181    // Do not allow creation if namespace already exists.
182    if (!updateExisting && namespaceExists) {
183      servlet.getMetrics().incrementFailedPutRequests(1);
184      return Response.status(Response.Status.FORBIDDEN).type(MIMETYPE_TEXT).entity("Namespace '"
185        + namespace + "' already exists.  Use REST PUT " + "to alter the existing namespace.")
186        .build();
187    }
188
189    // Do not allow altering if namespace does not exist.
190    if (updateExisting && !namespaceExists) {
191      servlet.getMetrics().incrementFailedPutRequests(1);
192      return Response.status(Response.Status.FORBIDDEN).type(MIMETYPE_TEXT).entity(
193        "Namespace '" + namespace + "' does not exist. Use " + "REST POST to create the namespace.")
194        .build();
195    }
196
197    return createOrUpdate(model, uriInfo, admin, updateExisting);
198  }
199
200  // Do the actual namespace create or alter.
201  private Response createOrUpdate(final NamespacesInstanceModel model, final UriInfo uriInfo,
202    final Admin admin, final boolean updateExisting) {
203    NamespaceDescriptor.Builder builder = NamespaceDescriptor.create(namespace);
204    builder.addConfiguration(model.getProperties());
205    if (model.getProperties().size() > 0) {
206      builder.addConfiguration(model.getProperties());
207    }
208    NamespaceDescriptor nsd = builder.build();
209
210    try {
211      if (updateExisting) {
212        admin.modifyNamespace(nsd);
213      } else {
214        admin.createNamespace(nsd);
215      }
216    } catch (IOException e) {
217      servlet.getMetrics().incrementFailedPutRequests(1);
218      return processException(e);
219    }
220
221    servlet.getMetrics().incrementSucessfulPutRequests(1);
222
223    return updateExisting
224      ? Response.ok(uriInfo.getAbsolutePath()).build()
225      : Response.created(uriInfo.getAbsolutePath()).build();
226  }
227
228  private boolean doesNamespaceExist(Admin admin, String namespaceName) throws IOException {
229    NamespaceDescriptor[] nd = admin.listNamespaceDescriptors();
230    for (int i = 0; i < nd.length; i++) {
231      if (nd[i].getName().equals(namespaceName)) {
232        return true;
233      }
234    }
235    return false;
236  }
237
238  /**
239   * Build a response for DELETE delete namespace.
240   * @param message value not used.
241   * @param headers value not used.
242   * @return response code.
243   */
244  @DELETE
245  public Response deleteNoBody(final byte[] message, final @Context UriInfo uriInfo,
246    final @Context HttpHeaders headers) {
247    if (LOG.isTraceEnabled()) {
248      LOG.trace("DELETE " + uriInfo.getAbsolutePath());
249    }
250    if (servlet.isReadOnly()) {
251      servlet.getMetrics().incrementFailedDeleteRequests(1);
252      return Response.status(Response.Status.FORBIDDEN).type(MIMETYPE_TEXT)
253        .entity("Forbidden" + CRLF).build();
254    }
255
256    try {
257      Admin admin = servlet.getAdmin();
258      if (!doesNamespaceExist(admin, namespace)) {
259        return Response.status(Response.Status.NOT_FOUND).type(MIMETYPE_TEXT)
260          .entity("Namespace '" + namespace + "' does not exists.  Cannot " + "drop namespace.")
261          .build();
262      }
263
264      admin.deleteNamespace(namespace);
265      servlet.getMetrics().incrementSucessfulDeleteRequests(1);
266      return Response.ok().build();
267
268    } catch (IOException e) {
269      servlet.getMetrics().incrementFailedDeleteRequests(1);
270      return processException(e);
271    }
272  }
273
274  /**
275   * Dispatch to NamespaceInstanceResource for getting list of tables.
276   */
277  @Path("tables")
278  public NamespacesInstanceResource
279    getNamespaceInstanceResource(final @PathParam("tables") String namespace) throws IOException {
280    return new NamespacesInstanceResource(this.namespace, true);
281  }
282}