001 /** 002 * Copyright (c) 2010 Yahoo! Inc. All rights reserved. 003 * Licensed under the Apache License, Version 2.0 (the "License"); 004 * you may not use this file except in compliance with the License. 005 * You may obtain a copy of the License at 006 * 007 * http://www.apache.org/licenses/LICENSE-2.0 008 * 009 * Unless required by applicable law or agreed to in writing, software 010 * distributed under the License is distributed on an "AS IS" BASIS, 011 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 012 * See the License for the specific language governing permissions and 013 * limitations under the License. See accompanying LICENSE file. 014 */ 015 package org.apache.oozie.servlet; 016 017 import org.apache.oozie.client.OozieClient.SYSTEM_MODE; 018 import org.apache.oozie.client.rest.JsonBean; 019 import org.apache.oozie.client.rest.RestConstants; 020 import org.apache.oozie.service.DagXLogInfoService; 021 import org.apache.oozie.service.InstrumentationService; 022 import org.apache.oozie.service.Services; 023 import org.apache.oozie.service.XLogService; 024 import org.apache.oozie.util.Instrumentation; 025 import org.apache.oozie.util.ParamChecker; 026 import org.apache.oozie.util.XLog; 027 import org.apache.oozie.ErrorCode; 028 import org.json.simple.JSONObject; 029 import org.json.simple.JSONStreamAware; 030 031 import javax.servlet.ServletConfig; 032 import javax.servlet.ServletException; 033 import javax.servlet.http.HttpServlet; 034 import javax.servlet.http.HttpServletRequest; 035 import javax.servlet.http.HttpServletResponse; 036 import java.io.IOException; 037 import java.util.ArrayList; 038 import java.util.Arrays; 039 import java.util.HashMap; 040 import java.util.List; 041 import java.util.Map; 042 import java.util.concurrent.atomic.AtomicLong; 043 044 /** 045 * Base class for Oozie web service API Servlets. <p/> This class provides common instrumentation, error logging and 046 * other common functionality. 047 */ 048 public abstract class JsonRestServlet extends HttpServlet { 049 050 private static final String JSTON_UTF8 = RestConstants.JSON_CONTENT_TYPE + "; charset=\"UTF-8\""; 051 052 protected static final String XML_UTF8 = RestConstants.XML_CONTENT_TYPE + "; charset=\"UTF-8\""; 053 054 protected static final String TEXT_UTF8 = RestConstants.TEXT_CONTENT_TYPE + "; charset=\"UTF-8\""; 055 056 protected static final String AUDIT_OPERATION = "audit.operation"; 057 protected static final String AUDIT_PARAM = "audit.param"; 058 protected static final String AUDIT_ERROR_CODE = "audit.error.code"; 059 protected static final String AUDIT_ERROR_MESSAGE = "audit.error.message"; 060 protected static final String AUDIT_HTTP_STATUS_CODE = "audit.http.status.code"; 061 062 private XLog auditLog; 063 XLog.Info logInfo; 064 065 /** 066 * This bean defines a query string parameter. 067 */ 068 public static class ParameterInfo { 069 private String name; 070 private Class type; 071 private List<String> methods; 072 private boolean required; 073 074 /** 075 * Creates a ParameterInfo with querystring parameter definition. 076 * 077 * @param name querystring parameter name. 078 * @param type type for the parameter value, valid types are: <code>Integer, Boolean and String</code> 079 * @param required indicates if the parameter is required. 080 * @param methods HTTP methods the parameter is used by. 081 */ 082 public ParameterInfo(String name, Class type, boolean required, List<String> methods) { 083 this.name = ParamChecker.notEmpty(name, "name"); 084 if (type != Integer.class && type != Boolean.class && type != String.class) { 085 throw new IllegalArgumentException("Type must be integer, boolean or string"); 086 } 087 this.type = ParamChecker.notNull(type, "type"); 088 this.required = required; 089 this.methods = ParamChecker.notNull(methods, "methods"); 090 } 091 092 } 093 094 /** 095 * This bean defines a REST resource. 096 */ 097 public static class ResourceInfo { 098 private String name; 099 private boolean wildcard; 100 private List<String> methods; 101 private Map<String, ParameterInfo> parameters = new HashMap<String, ParameterInfo>(); 102 103 /** 104 * Creates a ResourceInfo with a REST resource definition. 105 * 106 * @param name name of the REST resource, it can be an fixed resource name, empty or a wildcard ('*'). 107 * @param methods HTTP methods supported by the resource. 108 * @param parameters parameters supported by the resource. 109 */ 110 public ResourceInfo(String name, List<String> methods, List<ParameterInfo> parameters) { 111 this.name = name; 112 wildcard = name.equals("*"); 113 for (ParameterInfo parameter : parameters) { 114 this.parameters.put(parameter.name, parameter); 115 } 116 this.methods = ParamChecker.notNull(methods, "methods"); 117 } 118 } 119 120 /** 121 * Name of the instrumentation group for the WS layer, value is 'webservices'. 122 */ 123 protected static final String INSTRUMENTATION_GROUP = "webservices"; 124 125 private static final String INSTR_TOTAL_REQUESTS_SAMPLER = "requests"; 126 private static final String INSTR_TOTAL_REQUESTS_COUNTER = "requests"; 127 private static final String INSTR_TOTAL_FAILED_REQUESTS_COUNTER = "failed"; 128 private static AtomicLong TOTAL_REQUESTS_SAMPLER_COUNTER; 129 130 private Instrumentation instrumentation; 131 private String instrumentationName; 132 private AtomicLong samplerCounter = new AtomicLong(); 133 private ThreadLocal<Instrumentation.Cron> requestCron = new ThreadLocal<Instrumentation.Cron>(); 134 private List<ResourceInfo> resourcesInfo = new ArrayList<ResourceInfo>(); 135 private boolean allowSafeModeChanges; 136 137 /** 138 * Creates a servlet with a specified instrumentation sampler name for its requests. 139 * 140 * @param instrumentationName instrumentation name for timer and samplers for the servlet. 141 * @param resourcesInfo list of resource definitions supported by the servlet, empty and wildcard resources must be 142 * the last ones, in that order, first empty and the wildcard. 143 */ 144 public JsonRestServlet(String instrumentationName, ResourceInfo... resourcesInfo) { 145 this.instrumentationName = ParamChecker.notEmpty(instrumentationName, "instrumentationName"); 146 if (resourcesInfo.length == 0) { 147 throw new IllegalArgumentException("There must be at least one ResourceInfo"); 148 } 149 this.resourcesInfo = Arrays.asList(resourcesInfo); 150 auditLog = XLog.getLog("oozieaudit"); 151 auditLog.setMsgPrefix(""); 152 logInfo = new XLog.Info(XLog.Info.get()); 153 } 154 155 /** 156 * Enable HTTP POST/PUT/DELETE methods while in safe mode. 157 * 158 * @param allow <code>true</code> enabled safe mode changes, <code>false</code> disable safe mode changes 159 * (default). 160 */ 161 protected void setAllowSafeModeChanges(boolean allow) { 162 allowSafeModeChanges = allow; 163 } 164 165 /** 166 * Define an instrumentation sampler. <p/> Sampling period is 60 seconds, the sampling frequency is 1 second. <p/> 167 * The instrumentation group used is {@link #INSTRUMENTATION_GROUP}. 168 * 169 * @param samplerName sampler name. 170 * @param samplerCounter sampler counter. 171 */ 172 private void defineSampler(String samplerName, final AtomicLong samplerCounter) { 173 instrumentation.addSampler(INSTRUMENTATION_GROUP, samplerName, 60, 1, new Instrumentation.Variable<Long>() { 174 public Long getValue() { 175 return samplerCounter.get(); 176 } 177 }); 178 } 179 180 /** 181 * Add an instrumentation cron. 182 * 183 * @param name name of the timer for the cron. 184 * @param cron cron to add to a instrumentation timer. 185 */ 186 private void addCron(String name, Instrumentation.Cron cron) { 187 instrumentation.addCron(INSTRUMENTATION_GROUP, name, cron); 188 } 189 190 /** 191 * Start the request instrumentation cron. 192 */ 193 protected void startCron() { 194 requestCron.get().start(); 195 } 196 197 /** 198 * Stop the request instrumentation cron. 199 */ 200 protected void stopCron() { 201 requestCron.get().stop(); 202 } 203 204 /** 205 * Initializes total request and servlet request samplers. 206 */ 207 public void init(ServletConfig servletConfig) throws ServletException { 208 super.init(servletConfig); 209 instrumentation = Services.get().get(InstrumentationService.class).get(); 210 synchronized (JsonRestServlet.class) { 211 if (TOTAL_REQUESTS_SAMPLER_COUNTER == null) { 212 TOTAL_REQUESTS_SAMPLER_COUNTER = new AtomicLong(); 213 defineSampler(INSTR_TOTAL_REQUESTS_SAMPLER, TOTAL_REQUESTS_SAMPLER_COUNTER); 214 } 215 } 216 defineSampler(instrumentationName, samplerCounter); 217 } 218 219 /** 220 * Convenience method for instrumentation counters. 221 * 222 * @param name counter name. 223 * @param count count to increment the counter. 224 */ 225 private void incrCounter(String name, int count) { 226 if (instrumentation != null) { 227 instrumentation.incr(INSTRUMENTATION_GROUP, name, count); 228 } 229 } 230 231 /** 232 * Logs audit information for write requests to the audit log. 233 * 234 * @param request the http request. 235 */ 236 private void logAuditInfo(HttpServletRequest request) { 237 if (request.getAttribute(AUDIT_OPERATION) != null) { 238 Integer httpStatusCode = (Integer) request.getAttribute(AUDIT_HTTP_STATUS_CODE); 239 httpStatusCode = (httpStatusCode != null) ? httpStatusCode : HttpServletResponse.SC_OK; 240 String status = (httpStatusCode == HttpServletResponse.SC_OK) ? "SUCCESS" : "FAILED"; 241 String operation = (String) request.getAttribute(AUDIT_OPERATION); 242 String param = (String) request.getAttribute(AUDIT_PARAM); 243 String user = XLog.Info.get().getParameter(XLogService.USER); 244 String group = XLog.Info.get().getParameter(XLogService.GROUP); 245 String jobId = XLog.Info.get().getParameter(DagXLogInfoService.JOB); 246 String app = XLog.Info.get().getParameter(DagXLogInfoService.APP); 247 248 String errorCode = (String) request.getAttribute(AUDIT_ERROR_CODE); 249 String errorMessage = (String) request.getAttribute(AUDIT_ERROR_MESSAGE); 250 251 auditLog 252 .info( 253 "USER [{0}], GROUP [{1}], APP [{2}], JOBID [{3}], OPERATION [{4}], PARAMETER [{5}], STATUS [{6}], HTTPCODE [{7}], ERRORCODE [{8}], ERRORMESSAGE [{9}]", 254 user, group, app, jobId, operation, param, status, httpStatusCode, errorCode, errorMessage); 255 } 256 } 257 258 /** 259 * Dispatches to super after loginfo and intrumentation handling. In case of errors dispatches error response codes 260 * and does error logging. 261 */ 262 @SuppressWarnings("unchecked") 263 protected final void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, 264 IOException { 265 //if (Services.get().isSafeMode() && !request.getMethod().equals("GET") && !allowSafeModeChanges) { 266 if (Services.get().getSystemMode() != SYSTEM_MODE.NORMAL && !request.getMethod().equals("GET") && !allowSafeModeChanges) { 267 sendErrorResponse(response, HttpServletResponse.SC_SERVICE_UNAVAILABLE, ErrorCode.E0002.toString(), 268 ErrorCode.E0002.getTemplate()); 269 return; 270 } 271 Instrumentation.Cron cron = new Instrumentation.Cron(); 272 requestCron.set(cron); 273 try { 274 cron.start(); 275 validateRestUrl(request.getMethod(), getResourceName(request), request.getParameterMap()); 276 XLog.Info.get().clear(); 277 String user = getUser(request); 278 XLog.Info.get().setParameter(XLogService.USER, user); 279 TOTAL_REQUESTS_SAMPLER_COUNTER.incrementAndGet(); 280 samplerCounter.incrementAndGet(); 281 super.service(request, response); 282 } 283 catch (XServletException ex) { 284 XLog log = XLog.getLog(getClass()); 285 log.warn("URL[{0} {1}] error[{2}], {3}", request.getMethod(), getRequestUrl(request), ex.getErrorCode(), ex 286 .getMessage(), ex); 287 request.setAttribute(AUDIT_ERROR_MESSAGE, ex.getMessage()); 288 request.setAttribute(AUDIT_ERROR_CODE, ex.getErrorCode().toString()); 289 request.setAttribute(AUDIT_HTTP_STATUS_CODE, ex.getHttpStatusCode()); 290 incrCounter(INSTR_TOTAL_FAILED_REQUESTS_COUNTER, 1); 291 sendErrorResponse(response, ex.getHttpStatusCode(), ex.getErrorCode().toString(), ex.getMessage()); 292 } 293 catch (RuntimeException ex) { 294 XLog log = XLog.getLog(getClass()); 295 log.error("URL[{0} {1}] error, {2}", request.getMethod(), getRequestUrl(request), ex.getMessage(), ex); 296 incrCounter(INSTR_TOTAL_FAILED_REQUESTS_COUNTER, 1); 297 throw ex; 298 } 299 finally { 300 logAuditInfo(request); 301 TOTAL_REQUESTS_SAMPLER_COUNTER.decrementAndGet(); 302 incrCounter(INSTR_TOTAL_REQUESTS_COUNTER, 1); 303 samplerCounter.decrementAndGet(); 304 XLog.Info.remove(); 305 cron.stop(); 306 // TODO 307 incrCounter(instrumentationName, 1); 308 incrCounter(instrumentationName + "-" + request.getMethod(), 1); 309 addCron(instrumentationName, cron); 310 addCron(instrumentationName + "-" + request.getMethod(), cron); 311 requestCron.remove(); 312 } 313 } 314 315 private String getRequestUrl(HttpServletRequest request) { 316 StringBuffer url = request.getRequestURL(); 317 if (request.getQueryString() != null) { 318 url.append("?").append(request.getQueryString()); 319 } 320 return url.toString(); 321 } 322 323 /** 324 * Sends a JSON response. 325 * 326 * @param response servlet response. 327 * @param statusCode HTTP status code. 328 * @param bean bean to send as JSON response. 329 * @throws java.io.IOException thrown if the bean could not be serialized to the response output stream. 330 */ 331 protected void sendJsonResponse(HttpServletResponse response, int statusCode, JsonBean bean) throws IOException { 332 response.setStatus(statusCode); 333 JSONObject json = bean.toJSONObject(); 334 response.setContentType(JSTON_UTF8); 335 json.writeJSONString(response.getWriter()); 336 } 337 338 /** 339 * Sends a error response. 340 * 341 * @param response servlet response. 342 * @param statusCode HTTP status code. 343 * @param error error code. 344 * @param message error message. 345 * @throws java.io.IOException thrown if the error response could not be set. 346 */ 347 protected void sendErrorResponse(HttpServletResponse response, int statusCode, String error, String message) 348 throws IOException { 349 response.setHeader(RestConstants.OOZIE_ERROR_CODE, error); 350 response.setHeader(RestConstants.OOZIE_ERROR_MESSAGE, message); 351 response.sendError(statusCode); 352 } 353 354 protected void sendJsonResponse(HttpServletResponse response, int statusCode, JSONStreamAware json) 355 throws IOException { 356 if (statusCode == HttpServletResponse.SC_OK || statusCode == HttpServletResponse.SC_CREATED) { 357 response.setStatus(statusCode); 358 } 359 else { 360 response.sendError(statusCode); 361 } 362 response.setStatus(statusCode); 363 response.setContentType(JSTON_UTF8); 364 json.writeJSONString(response.getWriter()); 365 } 366 367 /** 368 * Validates REST URL using the ResourceInfos of the servlet. 369 * 370 * @param method HTTP method. 371 * @param resourceName resource name. 372 * @param queryStringParams query string parameters. 373 * @throws javax.servlet.ServletException thrown if the resource name or parameters are incorrect. 374 */ 375 @SuppressWarnings("unchecked") 376 protected void validateRestUrl(String method, String resourceName, Map<String, String[]> queryStringParams) 377 throws ServletException { 378 379 if (resourceName.contains("/")) { 380 throw new XServletException(HttpServletResponse.SC_BAD_REQUEST, ErrorCode.E0301, resourceName); 381 } 382 383 boolean valid = false; 384 for (int i = 0; !valid && i < resourcesInfo.size(); i++) { 385 ResourceInfo resourceInfo = resourcesInfo.get(i); 386 if (resourceInfo.name.equals(resourceName) || resourceInfo.wildcard) { 387 if (!resourceInfo.methods.contains(method)) { 388 throw new XServletException(HttpServletResponse.SC_BAD_REQUEST, ErrorCode.E0301, resourceName); 389 } 390 for (Map.Entry<String, String[]> entry : queryStringParams.entrySet()) { 391 String name = entry.getKey(); 392 ParameterInfo parameterInfo = resourceInfo.parameters.get(name); 393 if (parameterInfo != null) { 394 if (!parameterInfo.methods.contains(method)) { 395 throw new XServletException(HttpServletResponse.SC_BAD_REQUEST, ErrorCode.E0302, name); 396 } 397 String value = entry.getValue()[0].trim(); 398 if (parameterInfo.type.equals(Boolean.class)) { 399 value = value.toLowerCase(); 400 if (!value.equals("true") && !value.equals("false")) { 401 throw new XServletException(HttpServletResponse.SC_BAD_REQUEST, ErrorCode.E0304, name, 402 "boolean"); 403 } 404 } 405 if (parameterInfo.type.equals(Integer.class)) { 406 try { 407 Integer.parseInt(value); 408 } 409 catch (NumberFormatException ex) { 410 throw new XServletException(HttpServletResponse.SC_BAD_REQUEST, ErrorCode.E0304, name, 411 "integer"); 412 } 413 } 414 } 415 } 416 for (ParameterInfo parameterInfo : resourceInfo.parameters.values()) { 417 if (parameterInfo.methods.contains(method) && parameterInfo.required 418 && queryStringParams.get(parameterInfo.name) == null) { 419 throw new XServletException(HttpServletResponse.SC_BAD_REQUEST, ErrorCode.E0305, 420 parameterInfo.name); 421 } 422 } 423 valid = true; 424 } 425 } 426 if (!valid) { 427 throw new XServletException(HttpServletResponse.SC_BAD_REQUEST, ErrorCode.E0301, resourceName); 428 } 429 } 430 431 /** 432 * Return the resource name of the request. <p/> The resource name is the whole extra path. If the extra path starts 433 * with '/', the first '/' is trimmed. 434 * 435 * @param request request instance 436 * @return the resource name, <code>null</code> if none. 437 */ 438 protected String getResourceName(HttpServletRequest request) { 439 String requestPath = request.getPathInfo(); 440 if (requestPath != null) { 441 while (requestPath.startsWith("/")) { 442 requestPath = requestPath.substring(1); 443 } 444 requestPath = requestPath.trim(); 445 } 446 else { 447 requestPath = ""; 448 } 449 return requestPath; 450 } 451 452 /** 453 * Return the request content type, lowercase and without attributes. 454 * 455 * @param request servlet request. 456 * @return the request content type, <code>null</code> if none. 457 */ 458 protected String getContentType(HttpServletRequest request) { 459 String contentType = request.getContentType(); 460 if (contentType != null) { 461 int index = contentType.indexOf(";"); 462 if (index > -1) { 463 contentType = contentType.substring(0, index); 464 } 465 contentType = contentType.toLowerCase(); 466 } 467 return contentType; 468 } 469 470 /** 471 * Validate and return the content type of the request. 472 * 473 * @param request servlet request. 474 * @param expected expected contentType. 475 * @return the normalized content type (lowercase and without modifiers). 476 * @throws XServletException thrown if the content type is invalid. 477 */ 478 protected String validateContentType(HttpServletRequest request, String expected) throws XServletException { 479 String contentType = getContentType(request); 480 if (contentType == null || contentType.trim().length() == 0) { 481 throw new XServletException(HttpServletResponse.SC_BAD_REQUEST, ErrorCode.E0300, contentType); 482 } 483 if (!contentType.equals(expected)) { 484 throw new XServletException(HttpServletResponse.SC_BAD_REQUEST, ErrorCode.E0300, contentType); 485 } 486 return contentType; 487 } 488 489 /** 490 * Request attribute constant for the authenticatio token. 491 */ 492 public static final String AUTH_TOKEN = "oozie.auth.token"; 493 494 /** 495 * Request attribute constant for the user name. 496 */ 497 public static final String USER_NAME = "oozie.user.name"; 498 499 protected static final String UNDEF = "?"; 500 501 /** 502 * Return the authentication token of the request if any. 503 * 504 * @param request request. 505 * @return the authentication token, <code>null</code> if there is none. 506 */ 507 protected String getAuthToken(HttpServletRequest request) { 508 String authToken = (String) request.getAttribute(AUTH_TOKEN); 509 return (authToken != null) ? authToken : UNDEF; 510 } 511 512 /** 513 * Return the user name of the request if any. 514 * 515 * @param request request. 516 * @return the user name, <code>null</code> if there is none. 517 */ 518 protected String getUser(HttpServletRequest request) { 519 String userName = (String) request.getAttribute(USER_NAME); 520 return (userName != null) ? userName : UNDEF; 521 } 522 523 /** 524 * Set the log info with the given information. 525 * 526 * @param jobid job ID. 527 * @param actionid action ID. 528 */ 529 protected void setLogInfo(String jobid, String actionid) { 530 logInfo.setParameter(DagXLogInfoService.JOB, jobid); 531 logInfo.setParameter(DagXLogInfoService.ACTION, actionid); 532 533 XLog.Info.get().setParameters(logInfo); 534 } 535 }