/**
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License.  You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.apache.hadoop.mapred;

import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;

import javax.servlet.ServletContext;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.apache.hadoop.classification.InterfaceAudience;
import org.apache.hadoop.classification.InterfaceStability;
import org.apache.hadoop.fs.Path;
import org.apache.hadoop.mapreduce.JobACL;
import static org.apache.hadoop.mapred.QueueManager.toFullPropertyName;
import org.apache.hadoop.security.AccessControlException;
import org.apache.hadoop.security.UserGroupInformation;
import org.apache.hadoop.security.authorize.AccessControlList;
import org.apache.hadoop.http.HtmlQuoting;
import org.apache.hadoop.util.StringUtils;

/**
 * A servlet that is run by the TaskTrackers to provide the task logs via http.
 */
@InterfaceAudience.Private
@InterfaceStability.Unstable
public class TaskLogServlet extends HttpServlet {
  private static final long serialVersionUID = -6615764817774487321L;

  private static final Log LOG =
    LogFactory.getLog(TaskLog.class);

  private boolean haveTaskLog(TaskAttemptID taskId, boolean isCleanup,
      TaskLog.LogName type) {
    File f = TaskLog.getTaskLogFile(taskId, isCleanup, type);
    return f.canRead();
  }

  /**
   * Construct the taskLogUrl
   * @param taskTrackerHostName
   * @param httpPort
   * @param taskAttemptID
   * @return the taskLogUrl
   */
  public static String getTaskLogUrl(String taskTrackerHostName,
      String httpPort, String taskAttemptID) {
    return ("http://" + taskTrackerHostName + ":" + httpPort
        + "/tasklog?attemptid=" + taskAttemptID);
  }

  private void printTaskLog(HttpServletResponse response,
                            OutputStream out, TaskAttemptID taskId, 
                            long start, long end, boolean plainText, 
                            TaskLog.LogName filter, boolean isCleanup) 
  throws IOException {
    if (!plainText) {
      out.write(("<br><b><u>" + filter + " logs</u></b><br>\n" +
                 "<pre>\n").getBytes());
    }

    try {
      InputStream taskLogReader = 
        new TaskLog.Reader(taskId, filter, start, end, isCleanup);
      byte[] b = new byte[65536];
      int result;
      while (true) {
        result = taskLogReader.read(b);
        if (result > 0) {
          if (plainText) {
            out.write(b, 0, result); 
          } else {
            HtmlQuoting.quoteHtmlChars(out, b, 0, result);
          }
        } else {
          break;
        }
      }
      taskLogReader.close();
      if( !plainText ) {
        out.write("</pre></td></tr></table><hr><br>\n".getBytes());
      }
    } catch (IOException ioe) {
      if (filter == TaskLog.LogName.DEBUGOUT) {
        if (!plainText) {
           out.write("</pre><hr><br>\n".getBytes());
         }
        // do nothing
      }
      else {
        String msg = "Failed to retrieve " + filter + " log for task: " + 
                     taskId;
        LOG.warn(msg, ioe);
        response.sendError(HttpServletResponse.SC_GONE, msg);
      }
    }
  }

  /**
   * Validates if the given user has job view permissions for this job.
   * conf contains jobOwner and job-view-ACLs.
   * We allow jobOwner, superUser(i.e. mrOwner) and cluster administrators and
   * users and groups specified in configuration using
   * mapreduce.job.acl-view-job to view job.
   */
  private void checkAccessForTaskLogs(JobConf conf, String user, String jobId,
      TaskTracker tracker) throws AccessControlException {

    if (!tracker.areACLsEnabled()) {
      return;
    }

    // buiild job view ACL by reading from conf
    AccessControlList jobViewACL = tracker.getJobACLsManager().
        constructJobACLs(conf).get(JobACL.VIEW_JOB);

    // read job queue name from conf
    String queue = conf.getQueueName();

    // build queue admins ACL by reading from conf
    AccessControlList queueAdminsACL = new AccessControlList(
        conf.get(toFullPropertyName(queue,
            QueueACL.ADMINISTER_JOBS.getAclName()), " "));

    String jobOwner = conf.get(JobContext.USER_NAME);
    UserGroupInformation callerUGI =
        UserGroupInformation.createRemoteUser(user);

    // check if user is queue admin or cluster admin or jobOwner or member of
    // job-view-acl
    if (!queueAdminsACL.isUserAllowed(callerUGI)) {
      tracker.getACLsManager().checkAccess(jobId, callerUGI, queue,
          Operation.VIEW_TASK_LOGS, jobOwner, jobViewACL);
    }
  }

  /**
   * Builds a JobConf object by reading the job-acls.xml file.
   * This doesn't load the default resources.
   *
   * Returns null if job-acls.xml is not there in userlogs/$jobid on
   * local file system. This can happen when we restart the cluster with job
   * level authorization enabled(but was disabled on earlier cluster) and
   * viewing task logs of old jobs(i.e. jobs finished on earlier unsecure
   * cluster).
   */
  static JobConf getConfFromJobACLsFile(JobID jobId) {
    Path jobAclsFilePath = new Path(
        TaskLog.getJobDir(jobId).toString(),
        TaskTracker.jobACLsFile);
    JobConf conf = null;
    if (new File(jobAclsFilePath.toUri().getPath()).exists()) {
      conf = new JobConf(false);
      conf.addResource(jobAclsFilePath);
    }
    return conf;
  }

  /**
   * Get the logs via http.
   */
  @Override
  public void doGet(HttpServletRequest request, 
                    HttpServletResponse response
                    ) throws ServletException, IOException {
    long start = 0;
    long end = -1;
    boolean plainText = false;
    TaskLog.LogName filter = null;
    boolean isCleanup = false;

    String attemptIdStr = request.getParameter("attemptid");
    if (attemptIdStr == null) {
      response.sendError(HttpServletResponse.SC_BAD_REQUEST, 
                         "Argument attemptid is required");
      return;
    }

    String logFilter = request.getParameter("filter");
    if (logFilter != null) {
      try {
        filter = TaskLog.LogName.valueOf(TaskLog.LogName.class, 
                                         logFilter.toUpperCase());
      } catch (IllegalArgumentException iae) {
        response.sendError(HttpServletResponse.SC_BAD_REQUEST,
                           "Illegal value for filter: " + logFilter);
        return;
      }
    }
    
    String sLogOff = request.getParameter("start");
    if (sLogOff != null) {
      start = Long.valueOf(sLogOff).longValue();
    }
    
    String sLogEnd = request.getParameter("end");
    if (sLogEnd != null) {
      end = Long.valueOf(sLogEnd).longValue();
    }
    
    String sPlainText = request.getParameter("plaintext");
    if (sPlainText != null) {
      plainText = Boolean.valueOf(sPlainText);
    }
    
    String sCleanup = request.getParameter("cleanup");
    if (sCleanup != null) {
      isCleanup = Boolean.valueOf(sCleanup);
    }
    
    TaskAttemptID attemptId = TaskAttemptID.forName(attemptIdStr);
    if (!TaskLog.getAttemptDir(attemptId, isCleanup).exists()) {
      response.sendError(HttpServletResponse.SC_GONE,
          "Task log directory for task " + attemptId +
          " does not exist. May be cleaned up by Task Tracker, if older logs.");
      return;
    }

    // get user name who is accessing
    String user = request.getRemoteUser();
    if (user != null) {
      ServletContext context = getServletContext();
      TaskTracker taskTracker = (TaskTracker) context.getAttribute(
          "task.tracker");
      JobID jobId = attemptId.getJobID();

      // get jobACLConf from ACLs file
      JobConf jobACLConf = getConfFromJobACLsFile(jobId);
      // Ignore authorization if job-acls.xml is not found
      if (jobACLConf != null) {
        try {
          checkAccessForTaskLogs(jobACLConf, user,
              jobId.toString(), taskTracker);
        } catch (AccessControlException e) {
          String errMsg = "User " + user + " failed to view tasklogs of job " +
              jobId + "!\n\n" + e.getMessage();
          response.sendError(HttpServletResponse.SC_UNAUTHORIZED, errMsg);
          return;
        }
      }
    }

    OutputStream out = response.getOutputStream();
    if( !plainText ) {
      response.setContentType("text/html; charset=utf-8");
      out.write(("<html>\n" +
                 "<title>Task Logs: '" + attemptId + "'</title>\n" +
                 "<body>\n" +
                 "<h1>Task Logs: '" +  attemptId +  "'</h1><br>\n").getBytes()); 

      if (filter == null) {
        printTaskLog(response, out, attemptId, start, end, plainText, 
                     TaskLog.LogName.STDOUT, isCleanup);
        printTaskLog(response, out, attemptId, start, end, plainText, 
                     TaskLog.LogName.STDERR, isCleanup);
        printTaskLog(response, out, attemptId, start, end, plainText,
                     TaskLog.LogName.SYSLOG, isCleanup);
        if (haveTaskLog(attemptId, isCleanup, TaskLog.LogName.DEBUGOUT)) {
          printTaskLog(response, out, attemptId, start, end, plainText, 
                       TaskLog.LogName.DEBUGOUT, isCleanup);
        }
        if (haveTaskLog(attemptId, isCleanup, TaskLog.LogName.PROFILE)) {
          printTaskLog(response, out, attemptId, start, end, plainText, 
                       TaskLog.LogName.PROFILE, isCleanup);
        }
      } else {
        printTaskLog(response, out, attemptId, start, end, plainText, filter,
                     isCleanup);
      }
      
      out.write("</body></html>\n".getBytes());
      out.close();
    } else if (filter == null) {
      response.sendError(HttpServletResponse.SC_BAD_REQUEST,
          "You must supply a value for `filter' (STDOUT, STDERR, or SYSLOG) if you set plainText = true");
    } else {
      response.setContentType("text/plain; charset=utf-8");
      printTaskLog(response, out, attemptId, start, end, plainText, filter, 
                   isCleanup);
    } 
  }
}
