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