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