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