
com.dell.doradus.service.rest.RESTServlet Maven / Gradle / Ivy
The newest version!
/*
* Copyright (C) 2014 Dell, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.dell.doradus.service.rest;
import java.io.IOException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.dell.doradus.common.ApplicationDefinition;
import com.dell.doradus.common.HttpCode;
import com.dell.doradus.common.HttpDefs;
import com.dell.doradus.common.HttpMethod;
import com.dell.doradus.common.Pair;
import com.dell.doradus.common.RESTResponse;
import com.dell.doradus.common.UserDefinition.Permission;
import com.dell.doradus.common.Utils;
import com.dell.doradus.core.DoradusServer;
import com.dell.doradus.service.db.DBNotAvailableException;
import com.dell.doradus.service.db.DuplicateException;
import com.dell.doradus.service.db.Tenant;
import com.dell.doradus.service.db.UnauthorizedException;
import com.dell.doradus.service.schema.SchemaService;
import com.dell.doradus.service.tenant.TenantService;
/**
* An HttpServlet implementation used by the {@link RESTService} to process REST requests.
* Each request is matched to a registered REST command and, if found, is executed by
* invoking the command's callback. If the the request is unknown, a 404 response is
* return. Exceptions thrown by the callback are mapped to 4xx or 5xx responses as
* appropriate.
*/
public class RESTServlet extends HttpServlet {
private static final long serialVersionUID = 1L;
private final Logger m_logger = LoggerFactory.getLogger(getClass().getSimpleName());
//----- Inherited methods from HttpServlet
/**
* @see HttpServlet#doGet(HttpServletRequest request, HttpServletResponse response)
*/
@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
try {
long startNano = System.nanoTime();
RESTService.instance().onNewrequest();
RESTResponse restResponse = validateAndExecuteRequest(request);
if (restResponse.getCode().getCode() >= 300) {
RESTService.instance().onRequestRejected(restResponse.getCode().toString());
} else {
RESTService.instance().onRequestSuccess(startNano);
}
sendResponse(response, restResponse);
m_logger.debug("Elapsed time: {} millis; request={}",
(float)(System.nanoTime() - startNano)/1000000, getFullURI(request));
} catch (IllegalArgumentException e) {
// 400 Bad Request
RESTResponse restResponse = new RESTResponse(HttpCode.BAD_REQUEST, e.getMessage());
m_logger.info("Returning client error: {}; request: {}",
restResponse.toString(), getFullURI(request));
RESTService.instance().onRequestRejected(restResponse.getCode().toString());
sendResponse(response, restResponse);
} catch (NotFoundException e) {
// 404 Not Found
RESTResponse restResponse = new RESTResponse(HttpCode.NOT_FOUND, e.getMessage());
m_logger.info("Returning client error: {}; request: {}",
restResponse.toString(), getFullURI(request));
RESTService.instance().onRequestRejected(restResponse.getCode().toString());
sendResponse(response, restResponse);
} catch (DBNotAvailableException e) {
// 503 Service Unavailable
RESTResponse restResponse = new RESTResponse(HttpCode.SERVICE_UNAVAILABLE, e.getMessage());
m_logger.info("Returning service error: {}; request: {}",
restResponse.toString(), getFullURI(request));
RESTService.instance().onRequestRejected(restResponse.getCode().toString());
sendResponse(response, restResponse);
} catch (UnauthorizedException e) {
// 401 Unauthorized
RESTResponse restResponse = new RESTResponse(HttpCode.UNAUTHORIZED, e.getMessage());
m_logger.info("Returning client error: {}; request: {}",
restResponse.toString(), getFullURI(request));
RESTService.instance().onRequestRejected(restResponse.getCode().toString());
sendResponse(response, restResponse);
} catch (DuplicateException e) {
// 409 Conflict
RESTResponse restResponse = new RESTResponse(HttpCode.CONFLICT, e.getMessage());
m_logger.info("Returning client error: {}; request: {}",
restResponse.toString(), getFullURI(request));
RESTService.instance().onRequestRejected(restResponse.getCode().toString());
sendResponse(response, restResponse);
} catch (OutOfMemoryError e) {
// Report a fatal exception then shutdown. This is considered a better
// response to OOM then than continuing in an unstable state.
m_logger.error("Fatal: shutting down. Consider increasing memory or reducing " +
"the size of large input batches or queries.", e);
DoradusServer.shutDown();
System.exit(1);
} catch (Throwable e) {
// 500 Internal Error: include a stack trace and report in log.
m_logger.error("Unexpected exception handling request: " + getFullURI(request), e);
String stackTrace = Utils.getStackTrace(e);
RESTResponse restResponse = new RESTResponse(HttpCode.INTERNAL_ERROR, stackTrace);
RESTService.instance().onRequestFailed(e);
sendResponse(response, restResponse);
}
} // goGet
/**
* @see HttpServlet#doPost(HttpServletRequest request, HttpServletResponse response)
*/
@Override
protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
doGet(request, response);
} // doPost
/**
* @see HttpServlet#doPut(HttpServletRequest request, HttpServletResponse response)
*/
@Override
protected void doPut(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
doGet(request, response);
} // doPut
/**
* @see HttpServlet#doDelete(HttpServletRequest request, HttpServletResponse response)
*/
@Override
protected void doDelete(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
doGet(request, response);
} // doDelete
//----- Private methods
// Execute the given request and return a RESTResponse or throw an appropriate error.
private RESTResponse validateAndExecuteRequest(HttpServletRequest request) {
Map variableMap = new HashMap();
String query = extractQueryParam(request, variableMap);
Tenant tenant = getTenant(variableMap);
// Command matching expects an encoded URI but without the servlet context, if any.
String uri = request.getRequestURI();
String context = request.getContextPath();
if (!Utils.isEmpty(context)) {
uri = uri.substring(context.length());
}
ApplicationDefinition appDef = getApplication(uri, tenant);
HttpMethod method = HttpMethod.methodFromString(request.getMethod());
if (method == null) {
throw new NotFoundException("Request does not match a known URI: " + request.getRequestURL());
}
RegisteredCommand cmdModel = RESTService.instance().findCommand(appDef, method, uri, query, variableMap);
if (cmdModel == null) {
throw new NotFoundException("Request does not match a known URI: " + request.getRequestURL());
}
validateTenantAccess(request, tenant, cmdModel);
RESTCallback callback = cmdModel.getNewCallback();
callback.setRequest(new RESTRequest(tenant, appDef, request, variableMap));
return callback.invoke();
}
// Get the definition of the referenced application or null if there is none.
private ApplicationDefinition getApplication(String uri, Tenant tenant) {
if (uri.length() < 2 || uri.startsWith("/_")) {
return null; // Non-application request
}
String[] pathNodes = uri.substring(1).split("/");
String appName = Utils.urlDecode(pathNodes[0]);
ApplicationDefinition appDef = SchemaService.instance().getApplication(tenant, appName);
if (appDef == null) {
throw new NotFoundException("Unknown application: " + appName);
}
return appDef;
}
// Decide the Tenant context for this command and multi-tenant configuration options.
private Tenant getTenant(Map variableMap) {
String tenantName = variableMap.get("tenant");
if (Utils.isEmpty(tenantName)) {
return TenantService.instance().getDefaultTenant();
} else {
return new Tenant(tenantName); // might not exist
}
}
// Extract Authorization header, if any, and validate this command for the given tenant.
private void validateTenantAccess(HttpServletRequest request, Tenant tenant, RegisteredCommand cmdModel) {
String authString = request.getHeader("Authorization");
StringBuilder userID = new StringBuilder();
StringBuilder password = new StringBuilder();
decodeAuthorizationHeader(authString, userID, password);
TenantService.instance().validateTenantAuthorization(tenant, userID.toString(), password.toString(),
permissionForMethod(request.getMethod()), cmdModel.isPrivileged());
}
// Map an HTTP method to the permission needed to execute it.
private Permission permissionForMethod(String method) {
switch (method.toUpperCase()) {
case "GET":
return Permission.READ;
case "PUT":
case "DELETE":
return Permission.UPDATE;
case "POST":
return Permission.APPEND;
default:
throw new RuntimeException("Unexpected REST method: " + method);
}
}
// Decode the given Authorization header value into its user/password components.
private void decodeAuthorizationHeader(String authString, StringBuilder userID, StringBuilder password) {
userID.setLength(0);
password.setLength(0);
if (!Utils.isEmpty(authString) && authString.toLowerCase().startsWith("basic ")) {
String decoded = Utils.base64ToString(authString.substring("basic ".length()));
int inx = decoded.indexOf(':');
if (inx < 0) {
userID.append(decoded);
} else {
userID.append(decoded.substring(0, inx));
password.append(decoded.substring(inx + 1));
}
}
}
// Send the given response, which includes a response code and optionally a body
// and/or additional response headers. If the body is non-empty, we automatically add
// the Content-Length and a Content-Type of Text/plain.
private void sendResponse(HttpServletResponse servletResponse, RESTResponse restResponse) throws IOException {
servletResponse.setStatus(restResponse.getCode().getCode());
Map responseHeaders = restResponse.getHeaders();
if (responseHeaders != null) {
for (Map.Entry mapEntry : responseHeaders.entrySet()) {
if (mapEntry.getKey().equalsIgnoreCase(HttpDefs.CONTENT_TYPE)) {
servletResponse.setContentType(mapEntry.getValue());
} else {
servletResponse.setHeader(mapEntry.getKey(), mapEntry.getValue());
}
}
}
byte[] bodyBytes = restResponse.getBodyBytes();
int bodyLen = bodyBytes == null ? 0 : bodyBytes.length;
servletResponse.setContentLength(bodyLen);
if (bodyLen > 0 && servletResponse.getContentType() == null) {
servletResponse.setContentType("text/plain");
}
if (bodyLen > 0) {
servletResponse.getOutputStream().write(restResponse.getBodyBytes());
}
} // sendResponse
// Extract and return the query component of the given request, but move "api=x",
// "format=y", and "tenant=z", if present, to rest parameters.
private String extractQueryParam(HttpServletRequest request, Map restParams) {
String query = request.getQueryString();
if (Utils.isEmpty(query)) {
return "";
}
StringBuilder buffer = new StringBuilder(query);
// Split query component into decoded, &-separate components.
String[] parts = Utils.splitURIQuery(buffer.toString());
List> unusedList = new ArrayList>();
boolean bRewrite = false;
for (String part : parts) {
Pair param = extractParam(part);
switch (param.firstItemInPair.toLowerCase()) {
case "api":
bRewrite = true;
restParams.put("api", param.secondItemInPair);
break;
case "format":
bRewrite = true;
if (param.secondItemInPair.equalsIgnoreCase("xml")) {
restParams.put("format", "text/xml");
} else if (param.secondItemInPair.equalsIgnoreCase("json")) {
restParams.put("format", "application/json");
}
break;
case "tenant":
bRewrite = true;
restParams.put("tenant", param.secondItemInPair);
break;
default:
unusedList.add(param);
}
}
// If we extracted any fixed params, rewrite the query parameter.
if (bRewrite) {
buffer.setLength(0);
for (Pair pair : unusedList) {
if (buffer.length() > 0) {
buffer.append("&");
}
buffer.append(Utils.urlEncode(pair.firstItemInPair));
if (pair.secondItemInPair != null) {
buffer.append("=");
buffer.append(Utils.urlEncode(pair.secondItemInPair));
}
}
}
return buffer.toString();
} // extractQueryParam
// Split the given k=v (or just k) param into a Pair object.
private Pair extractParam(String part) {
int eqInx = part.indexOf('=');
String paramName;
String paramValue;
if (eqInx < 0) {
paramName = part;
paramValue = null;
} else {
paramName = part.substring(0, eqInx);
paramValue = part.substring(eqInx + 1);
}
return Pair.create(paramName, paramValue);
} // extractParam
// Reconstruct the entire URI from the given request.
private String getFullURI(HttpServletRequest request) {
StringBuilder buffer = new StringBuilder(request.getMethod());
buffer.append(" ");
buffer.append(request.getRequestURI());
String queryParam = request.getQueryString();
if (!Utils.isEmpty(queryParam)) {
buffer.append("?");
buffer.append(queryParam);
}
return buffer.toString();
} // getFullURI
} // class RESTServlet
© 2015 - 2025 Weber Informatics LLC | Privacy Policy