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