/**
 * 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.tez.dag.app.web;

import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import java.util.StringTokenizer;
import java.util.TreeMap;

import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Splitter;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.Sets;
import com.google.inject.Inject;
import com.google.inject.name.Named;

import org.apache.commons.lang.StringEscapeUtils;
import org.apache.tez.common.counters.CounterGroup;
import org.apache.tez.common.counters.LimitExceededException;
import org.apache.tez.common.counters.TezCounter;
import org.apache.tez.common.counters.TezCounters;
import org.apache.tez.dag.api.client.ProgressBuilder;
import org.apache.tez.dag.app.dag.Task;
import org.apache.tez.dag.app.dag.TaskAttempt;
import org.apache.tez.dag.records.TezTaskAttemptID;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.apache.hadoop.security.UserGroupInformation;
import org.apache.hadoop.yarn.webapp.Controller;
import org.apache.hadoop.yarn.webapp.MimeType;
import org.apache.hadoop.yarn.webapp.View;
import org.apache.hadoop.yarn.webapp.WebAppException;
import org.apache.tez.dag.api.TezConfiguration;
import org.apache.tez.dag.app.AppContext;
import org.apache.tez.dag.app.dag.DAG;
import org.apache.tez.dag.app.dag.Vertex;
import org.apache.tez.dag.records.TezDAGID;
import org.apache.tez.dag.records.TezVertexID;

public class AMWebController extends Controller {

  private final static Logger LOG = LoggerFactory.getLogger(AMWebController.class);

  // HTTP CORS Request Headers
  static final String ORIGIN = "Origin";

  // HTTP CORS Response Headers
  static final String ACCESS_CONTROL_ALLOW_ORIGIN = "Access-Control-Allow-Origin";
  static final String ACCESS_CONTROL_ALLOW_CREDENTIALS = "Access-Control-Allow-Credentials";
  static final String ACCESS_CONTROL_ALLOW_METHODS = "Access-Control-Allow-Methods";
  static final String ACCESS_CONTROL_ALLOW_HEADERS = "Access-Control-Allow-Headers";
  static final String ACCESS_CONTROL_MAX_AGE = "Access-Control-Max-Age";

  // CORS default responses.
  static final String ALLOWED_METHODS = "GET, HEAD";
  static final String ALLOWED_HEADERS = "X-Requested-With,Content-Type,Accept,Origin";

  static final String DAG_PROGRESS = "dagProgress";
  static final String VERTEX_PROGRESS = "vertexProgress";
  static final String VERTEX_PROGRESSES = "vertexProgresses";

  static final int MAX_QUERIED = 100;
  public static final String VERSION = "2";

  private AppContext appContext;
  private String historyUrl;

  @Inject
  public AMWebController(RequestContext requestContext,
                         AppContext appContext,
                         @Named("TezUIHistoryURL") String historyUrl) {
    super(requestContext);
    this.appContext = appContext;
    this.historyUrl = historyUrl;
  }

  @Override
  public void index() {
    ui();
  }

  public void ui() {
    render(StaticAMView.class);
  }

  public void main() {
    ui();
  }

  public void about() {
    renderJSON("Tez AM UI WebServices");
  }

  static String encodeHeader(final String header) {
    if (header == null) {
      return null;
    }
    // Protect against HTTP response splitting vulnerability
    // since value is written as part of the response header
    // Ensure this header only has one header by removing
    // CRs and LFs
    return header.split("\n|\r")[0].trim();
  }

  @VisibleForTesting
  public void setCorsHeaders() {
    final HttpServletResponse res = response();

    /*
     * ideally the Origin and other CORS headers should be checked and response headers set only
     * if it matches the allowed origins. however rm does not forward these headers.
     */
    String historyUrlBase = appContext.getAMConf().get(TezConfiguration.TEZ_HISTORY_URL_BASE, "");
    String origin = request().getHeader(ORIGIN);
    if(origin == null) {
      try {
        URL url = new URL(historyUrlBase);
        origin = url.getProtocol() + "://" + url.getAuthority();
      } catch (MalformedURLException e) {
        LOG.debug("Invalid url set for tez history url base: {}", historyUrlBase, e);
      }
    }

    if (origin != null) {
      origin = encodeHeader(origin);
      res.setHeader(ACCESS_CONTROL_ALLOW_ORIGIN, origin);
    }
    res.setHeader(ACCESS_CONTROL_ALLOW_METHODS, ALLOWED_METHODS);
    res.setHeader(ACCESS_CONTROL_ALLOW_CREDENTIALS, Boolean.TRUE.toString());
    res.setHeader(ACCESS_CONTROL_ALLOW_HEADERS, ALLOWED_HEADERS);
    res.setHeader(ACCESS_CONTROL_MAX_AGE, "1800");
  }

  void sendErrorResponse(int sc, String msg, Exception e) {
    LOG.debug(msg, e);

    try {
      response().sendError(sc, msg);
    } catch (IOException e1) {
      throw new WebAppException(e);
    }
  }

  @VisibleForTesting
  static boolean _hasAccess(UserGroupInformation callerUGI, AppContext appContext) {
    if (callerUGI == null) {
      // Allow anonymous access iff acls disabled
      return !appContext.getAMACLManager().isAclsEnabled();
    }
    return appContext.getAMACLManager().checkDAGViewAccess(callerUGI);
  }

  public boolean hasAccess() {
    String remoteUser = request().getRemoteUser();
    UserGroupInformation callerUGI = null;
    if (remoteUser != null && !remoteUser.isEmpty()) {
      callerUGI = UserGroupInformation.createRemoteUser(remoteUser);
    }

    return _hasAccess(callerUGI, appContext);
  }

  public void getDagProgress() {

    setCorsHeaders();

    if (!hasAccess()) {
      sendErrorResponse(HttpServletResponse.SC_UNAUTHORIZED, "Access denied for user: " +
          request().getRemoteUser(), null);
      return;
    }

    int dagID;
    try {
      dagID = getQueryParamInt(WebUIService.DAG_ID);
    } catch (NumberFormatException e) {
      sendErrorResponse(HttpServletResponse.SC_BAD_REQUEST, "Invalid dag id:", e);
      return;
    }

    DAG currentDAG = appContext.getCurrentDAG();

    if (currentDAG == null || dagID != currentDAG.getID().getId()) {
      sendErrorResponse(HttpServletResponse.SC_NOT_FOUND, "Not current Dag: " + dagID, null);
      return;
    }

    Map<String, ProgressInfo> result = new HashMap<String, ProgressInfo>();
    result.put(DAG_PROGRESS,
        new ProgressInfo(currentDAG.getID().toString(),
            currentDAG.getCompletedTaskProgress()));
    renderJSON(result);
  }

  public void getVertexProgress() {
    int dagID;
    int vertexID;

    setCorsHeaders();

    if (!hasAccess()) {
      sendErrorResponse(HttpServletResponse.SC_UNAUTHORIZED, "Access denied for user: " +
          request().getRemoteUser(), null);
      return;
    }

    try {
      dagID = getQueryParamInt(WebUIService.DAG_ID);
      vertexID = getQueryParamInt(WebUIService.VERTEX_ID);
    } catch (NumberFormatException e) {
      sendErrorResponse(HttpServletResponse.SC_BAD_REQUEST, "Invalid dag or vertex id", e);
      return;
    }

    DAG currentDAG = appContext.getCurrentDAG();

    if (currentDAG == null || currentDAG.getID().getId() != dagID) {
      sendErrorResponse(HttpServletResponse.SC_NOT_FOUND, "Not current Dag: " + dagID, null);
      return;
    }

    final TezVertexID tezVertexID = TezVertexID.getInstance(currentDAG.getID(), vertexID);
    Vertex vertex = currentDAG.getVertex(tezVertexID);
    if (vertex == null) {
      sendErrorResponse(HttpServletResponse.SC_NOT_FOUND, "vertex not found: " + vertexID, null);
      return;
    }

    Map<String, ProgressInfo> result = new HashMap<String, ProgressInfo>();
    result.put(VERTEX_PROGRESS, new ProgressInfo(tezVertexID.toString(),
        vertex.getCompletedTaskProgress()));
    renderJSON(result);
  }


  Collection<Vertex> getVerticesByIdx(DAG dag, Collection<Integer> indexes) {
    Collection<Vertex> vertices = new ArrayList<Vertex>(indexes.size());
    final TezDAGID tezDAGID = dag.getID();

    for (Integer idx : indexes) {
      final TezVertexID tezVertexID = TezVertexID.getInstance(tezDAGID, idx);
      if (tezVertexID == null) {
        continue;
      }
      final Vertex vertex = dag.getVertex(tezVertexID);
      if (vertex != null) {
        vertices.add(vertex);
      }
    }

    return  vertices;
  }

  int getQueryParamInt(String name) throws NumberFormatException {
    final String valueStr = $(name).trim();

    return Integer.parseInt(valueStr);
  }

  public void getVertexProgresses() {
    int dagID;

    setCorsHeaders();
    if (!hasAccess()) {
      sendErrorResponse(HttpServletResponse.SC_UNAUTHORIZED, "Access denied for user: " +
          request().getRemoteUser(), null);
      return;
    }

    List<Integer> vertexIDs = new ArrayList<Integer>();
    try {
      dagID = getQueryParamInt(WebUIService.DAG_ID);
      for (String vertexIDStr : $(WebUIService.VERTEX_ID).trim().split(",", MAX_QUERIED)) {
        vertexIDs.add(Integer.parseInt(vertexIDStr));
      }
    } catch (NumberFormatException e) {
      sendErrorResponse(HttpServletResponse.SC_BAD_REQUEST, "Invalid dag or vertices id", e);
      return;
    }

    DAG currentDAG = appContext.getCurrentDAG();
    if (currentDAG == null || currentDAG.getID().getId() != dagID) {
      sendErrorResponse(HttpServletResponse.SC_NOT_FOUND, "Not current Dag: " + dagID, null);
      return;
    }

    Collection<Vertex> vertices;
    if (vertexIDs.isEmpty()) {
      vertices = currentDAG.getVertices().values();
    } else {
      vertices = getVerticesByIdx(currentDAG, vertexIDs);
    }

    Collection<ProgressInfo> progresses = new ArrayList<ProgressInfo>(vertices.size());
    for(Vertex vertex : vertices) {
      progresses.add(new ProgressInfo(vertex.getVertexId().toString(),
          vertex.getCompletedTaskProgress()));
    }

    Map<String, Collection<ProgressInfo>> result = new HashMap<String, Collection<ProgressInfo>>();
    result.put(VERTEX_PROGRESSES, progresses);
    renderJSON(result);
  }

  // AM WebApi V2.
  @VisibleForTesting
  protected boolean setupResponse() {
    setCorsHeaders();

    if (!hasAccess()) {
      sendErrorResponse(HttpServletResponse.SC_UNAUTHORIZED, "Access denied for user: " +
          request().getRemoteUser(), null);
      return false;
    }

    return true;
  }

  DAG checkAndGetDAGFromRequest() {
    DAG dag = null;
    int errorCode = HttpServletResponse.SC_OK;
    String message = null;
    Exception ex = null;
    try {
      int dagID = getQueryParamInt(WebUIService.DAG_ID);
      dag = appContext.getCurrentDAG();
      if (dag == null || dag.getID().getId() != dagID) {
        errorCode = HttpServletResponse.SC_NOT_FOUND;
        message = "Not current Dag: " + dagID;
      }
    } catch (NumberFormatException e) {
      errorCode = HttpServletResponse.SC_BAD_REQUEST;
      message = "Invalid dag id";
      ex = e;
    }

    if (errorCode != HttpServletResponse.SC_OK) {
      dag = null;
      sendErrorResponse(errorCode, message, ex);
    }

    return dag;
  }

  Collection<Integer> getVertexIDsFromRequest() {
    final String valueStr = $(WebUIService.VERTEX_ID).trim();

    List<Integer> vertexIDs = new ArrayList<Integer>();
    if (!valueStr.equals("")) {
      String[] vertexIdsStr = valueStr.split(",", MAX_QUERIED);

      try {
        for (String vertexIdStr : vertexIdsStr) {
          int vertexId = Integer.parseInt(vertexIdStr);
          vertexIDs.add(vertexId);
        }
      } catch (NumberFormatException nfe) {
        sendErrorResponse(HttpServletResponse.SC_BAD_REQUEST,
            "invalid vertex ID passed in as parameter", nfe);
        vertexIDs = null;
      }
    }

    return vertexIDs;
  }

  /**
   * Parse a params list in the format: CtrGroup/CtrName1,CtrName2;CtrGroup2;
   * @return nested structure of counter groups and names. Null if nothing specified.
   */
  Map<String, Set<String>> getCounterListFromRequest() {
    final String counterStr = $(WebUIService.COUNTERS).trim();
    if (counterStr == null || counterStr.isEmpty()) {
      return null;
    }

    String delimiter = ";";
    String groupDelimiter = "/";
    String counterDelimiter = ",";

    StringTokenizer tokenizer = new StringTokenizer(counterStr, delimiter);

    Map<String, Set<String>> counterList = new TreeMap<String, Set<String>>();
    while (tokenizer.hasMoreElements()) {
      String token = tokenizer.nextToken().trim();
      int pos = token.indexOf(groupDelimiter);
      if (pos == -1) {
        counterList.put(token, Collections.<String>emptySet());
        continue;
      }
      String counterGroup = token.substring(0, pos);
      Set<String> counters = Collections.<String>emptySet();
      if (pos < token.length() - 1) {
        String counterNames = token.substring(pos+1, token.length());
        counters = Sets.newHashSet(
            Splitter.on(counterDelimiter).omitEmptyStrings()
                .trimResults().split(counterNames));
      }
      counterList.put(counterGroup, counters);
    }
    return counterList;
  }


  List<String> splitString(String str, String delimiter, Integer limit) {
    List<String> items = new ArrayList<String>();

    StringTokenizer tokenizer = new StringTokenizer(str, delimiter);
    for(int count = 0; tokenizer.hasMoreElements() && count < limit; count ++) {
      items.add(tokenizer.nextToken());
    }

    return items;
  }

  /**
   * getIntegersFromRequest
   * Parses a query parameter with comma separated values and returns an array of integers.
   * The function returns null if any of the value is not an integer
   *
   * @param paramName {String}
   * @param limit {Integer} Maximum number of values to be taken
   *
   * @return {List<Integer>} List of parsed values
   */
  List<Integer> getIntegersFromRequest(String paramName, Integer limit) {
    String valuesStr = $(paramName).trim();

    List<Integer> values = new ArrayList<Integer>();
    if (!valuesStr.equals("")) {
      try {
        for (String valueStr : splitString(valuesStr, ",", limit)) {
          int value = Integer.parseInt(valueStr);
          values.add(value);
        }
      } catch (NumberFormatException nfe) {
        sendErrorResponse(HttpServletResponse.SC_BAD_REQUEST,
            String.format("invalid %s passed in as parameter", paramName), nfe);
        values = null;
      }
    }

    return values;
  }

  /**
   * getTaskIDsFromRequest
   * Takes in "1_0,1_3" and returns [[1,0],[1,3]]
   * Mainly to parse a query parameter with comma separated indexes. For vertex its the index,
   * for task its vertexIndex_taskIndex and for attempts its vertexIndex_taskIndex_attemptNo
   * The function returns null if any of the value is not an integer
   *
   * @param paramName {String}
   * @param limit {Integer} Maximum number of values to be taken
   *
   * @return {List<List<Integer>>} List of parsed values
   */
  List<List<Integer>> getIDsFromRequest(String paramName, Integer limit, Integer count) {
    String valuesStr = $(paramName).trim();

    List<List<Integer>> values = new ArrayList<List<Integer>>();
    if (!valuesStr.equals("")) {
      try {
        for (String valueStr : splitString(valuesStr, ",", limit)) {
          List<Integer> innerValues = new ArrayList<Integer>();
          String innerValueStrs[] = valueStr.split("_");
          if(innerValueStrs.length == count) {
            for (String innerValueStr : innerValueStrs) {
              int value = Integer.parseInt(innerValueStr);
              innerValues.add(value);
            }
            values.add(innerValues);
          }
        }
      } catch (NumberFormatException nfe) {
        sendErrorResponse(HttpServletResponse.SC_BAD_REQUEST,
            String.format("invalid %s passed in as parameter", paramName), nfe);
        values = null;
      }
    }

    return values;
  }

  public void getDagInfo() {
    if (!setupResponse()) {
      return;
    }

    DAG dag = checkAndGetDAGFromRequest();
    if (dag == null) {
      return;
    }

    Map<String, Set<String>> counterNames = getCounterListFromRequest();

    Map<String, Object> dagInfo = new HashMap<String, Object>();
    dagInfo.put("id", dag.getID().toString());
    dagInfo.put("progress", Float.toString(dag.getCompletedTaskProgress()));
    dagInfo.put("status", dag.getState().toString());

    try {
      if (counterNames != null && !counterNames.isEmpty()) {
        TezCounters counters = dag.getCachedCounters();
        Map<String, Map<String, Long>> counterMap = constructCounterMapInfo(counters, counterNames);
        if (counterMap != null && !counterMap.isEmpty()) {
          dagInfo.put("counters", counterMap);
        }
      }
    } catch (LimitExceededException e) {
      // Ignore
      // TODO: add an error message instead for counter key
    }
    renderJSON(ImmutableMap.of(
        "dag", dagInfo
    ));
  }

  Map<String, Map<String, Long>> constructCounterMapInfo(TezCounters counters,
      Map<String, Set<String>> counterNames) {
    if (counterNames == null || counterNames.isEmpty()) {
      return null;
    }
    if (LOG.isDebugEnabled()) {
      LOG.debug("Requested counter names=" + counterNames.entrySet());
      LOG.debug("actual counters=" + counters);
    }

    Map<String, Map<String, Long>> counterInfo = new TreeMap<String, Map<String, Long>>();

    if (counterNames.containsKey("*")) {
      for (CounterGroup grpCounters : counters) {
        Map<String, Long> matchedCounters = new HashMap<String, Long>();
        for (TezCounter counter : grpCounters) {
          matchedCounters.put(counter.getName(), counter.getValue());
        }
        counterInfo.put(grpCounters.getName(), matchedCounters);
      }
    } else {
      for (Entry<String, Set<String>> entry : counterNames.entrySet()) {
        Map<String, Long> matchedCounters = new HashMap<String, Long>();
        CounterGroup grpCounters = counters.getGroup(entry.getKey());
        for (TezCounter counter : grpCounters) {
          if (entry.getValue().isEmpty() || entry.getValue().contains(counter.getName())) {
            matchedCounters.put(counter.getName(), counter.getValue());
          }
        }
        counterInfo.put(entry.getKey(), matchedCounters);
      }
    }

    return counterInfo;
  }

  private Map<String, Object> getVertexInfoMap(Vertex vertex,
                                               Map<String, Set<String>> counterNames) {
    Map<String, Object> vertexInfo = new HashMap<String, Object>();
    vertexInfo.put("id", vertex.getVertexId().toString());
    vertexInfo.put("status", vertex.getState().toString());
    vertexInfo.put("progress", Float.toString(vertex.getCompletedTaskProgress()));

    vertexInfo.put("initTime", Long.toString(vertex.getInitTime()));
    vertexInfo.put("startTime", Long.toString(vertex.getStartTime()));
    vertexInfo.put("finishTime", Long.toString(vertex.getFinishTime()));
    vertexInfo.put("firstTaskStartTime", Long.toString(vertex.getFirstTaskStartTime()));
    vertexInfo.put("lastTaskFinishTime", Long.toString(vertex.getLastTaskFinishTime()));

    ProgressBuilder vertexProgress = vertex.getVertexProgress();
    vertexInfo.put("totalTasks", Integer.toString(vertexProgress.getTotalTaskCount()));
    vertexInfo.put("runningTasks", Integer.toString(vertexProgress.getRunningTaskCount()));
    vertexInfo.put("succeededTasks", Integer.toString(vertexProgress.getSucceededTaskCount()));

    vertexInfo.put("failedTaskAttempts",
        Integer.toString(vertexProgress.getFailedTaskAttemptCount()));
    vertexInfo.put("killedTaskAttempts",
        Integer.toString(vertexProgress.getKilledTaskAttemptCount()));

    try {
      if (counterNames != null && !counterNames.isEmpty()) {
        TezCounters counters = vertex.getCachedCounters();
        Map<String, Map<String, Long>> counterMap = constructCounterMapInfo(counters, counterNames);
        if (counterMap != null && !counterMap.isEmpty()) {
          vertexInfo.put("counters", counterMap);
        }
      }
    } catch (LimitExceededException e) {
      // Ignore
      // TODO: add an error message instead for counter key
    }

    return vertexInfo;
  }

  public void getVerticesInfo() {
    if (!setupResponse()) {
      return;
    }

    DAG dag = checkAndGetDAGFromRequest();
    if (dag == null) {
      return;
    }

    Collection<Integer> requestedIDs = getVertexIDsFromRequest();

    if (requestedIDs == null) {
      return;
    }

    Map<String, Set<String>> counterNames = getCounterListFromRequest();

    Collection<Vertex> vertexList;
    if (requestedIDs.isEmpty()) {
      // no ids specified return all.
      vertexList = dag.getVertices().values();
    } else {
      vertexList = getVerticesByIdx(dag, requestedIDs);
    }

    ArrayList<Map<String, Object>> verticesInfo = new ArrayList<Map<String, Object>>();
    for(Vertex v : vertexList) {
      verticesInfo.add(getVertexInfoMap(v, counterNames));
    }

    renderJSON(ImmutableMap.of(
        "vertices", verticesInfo
    ));
  }

  Vertex getVertexFromIndex(DAG dag, Integer vertexIndex) {
    final TezVertexID tezVertexID = TezVertexID.getInstance(dag.getID(), vertexIndex);
    Vertex vertex = dag.getVertex(tezVertexID);
    return vertex;
  }

  /**
   * getRequestedTasks
   * Heart of getTasksInfo. Given a dag and a limit, based on the incoming query parameters
   * returns a list of task instances
   *
   * @param dag {DAG}
   * @param limit {Integer}
   */
  List<Task> getRequestedTasks(DAG dag, Integer limit) {
    List<Task> tasks = new ArrayList<Task>();

    List<List<Integer>> taskIDs = getIDsFromRequest(WebUIService.TASK_ID, limit, 2);
    if(taskIDs == null) {
      return null;
    }
    else if(!taskIDs.isEmpty()) {
      for (List<Integer> indexes : taskIDs) {
        Vertex vertex = getVertexFromIndex(dag, indexes.get(0));
        if(vertex == null) {
          continue;
        }
        Task task = vertex.getTask(indexes.get(1));
        if(task == null) {
          continue;
        }
        else {
          tasks.add(task);
        }

        if(tasks.size() >= limit) {
          break;
        }
      }
    }
    else {
      List<Integer> vertexIDs = getIntegersFromRequest(WebUIService.VERTEX_ID, limit);
      if(vertexIDs == null) {
        return null;
      }
      else if(!vertexIDs.isEmpty()) {
        for (Integer vertexID : vertexIDs) {
          Vertex vertex = getVertexFromIndex(dag, vertexID);
          if(vertex == null) {
            continue;
          }
          List<Task> vertexTasks = new ArrayList<Task>(vertex.getTasks().values());
          tasks.addAll(vertexTasks.subList(0, Math.min(vertexTasks.size(), limit - tasks.size())));

          if(tasks.size() >= limit) {
            break;
          }
        }
      }
      else {
        Collection<Vertex> vertices = dag.getVertices().values();
        for (Vertex vertex : vertices) {
          List<Task> vertexTasks = new ArrayList<Task>(vertex.getTasks().values());
          tasks.addAll(vertexTasks.subList(0, Math.min(vertexTasks.size(), limit - tasks.size())));

          if(tasks.size() >= limit) {
            break;
          }
        }
      }
    }

    return tasks;
  }

  /**
   * Renders the response JSON for tasksInfo API
   * The JSON will have an array of task objects under the key tasks.
   */
  public void getTasksInfo() {
    if (!setupResponse()) {
      return;
    }

    DAG dag = checkAndGetDAGFromRequest();
    if (dag == null) {
      return;
    }

    int limit = MAX_QUERIED;
    try {
      limit = getQueryParamInt(WebUIService.LIMIT);
    } catch (NumberFormatException e) {
      //Ignore
    }

    List<Task> tasks = getRequestedTasks(dag, limit);
    if(tasks == null) {
      return;
    }

    Map<String, Set<String>> counterNames = getCounterListFromRequest();

    ArrayList<Map<String, Object>> tasksInfo = new ArrayList<Map<String, Object>>();
    for(Task t : tasks) {
      Map<String, Object> taskInfo = new HashMap<String, Object>();
      taskInfo.put("id", t.getTaskID().toString());
      taskInfo.put("progress", Float.toString(t.getProgress()));
      taskInfo.put("status", t.getState().toString());

      try {
        TezCounters counters = t.getCounters();
        Map<String, Map<String, Long>> counterMap = constructCounterMapInfo(counters, counterNames);
        if (counterMap != null && !counterMap.isEmpty()) {
          taskInfo.put("counters", counterMap);
        }
      } catch (LimitExceededException e) {
        // Ignore
        // TODO: add an error message instead for counter key
      }

      tasksInfo.add(taskInfo);
    }

    renderJSON(ImmutableMap.of(
      "tasks", tasksInfo
    ));
  }

  /**
   * getRequestedAttempts
   * Given a dag and a limit, based on the incoming query parameters. Used by getAttemptsInfo
   * returns a list of task instances
   *
   * @param dag {DAG}
   * @param limit {Integer}
   */
  List<TaskAttempt> getRequestedAttempts(DAG dag, Integer limit) {
    List<TaskAttempt> attempts = new ArrayList<TaskAttempt>();

    List<List<Integer>> attemptIDs = getIDsFromRequest(WebUIService.ATTEMPT_ID, limit, 3);
    if(attemptIDs == null) {
      return null;
    }
    else if(!attemptIDs.isEmpty()) {
      for (List<Integer> indexes : attemptIDs) {
        Vertex vertex = getVertexFromIndex(dag, indexes.get(0));
        if(vertex == null) {
          continue;
        }
        Task task = vertex.getTask(indexes.get(1));
        if(task == null) {
          continue;
        }

        TaskAttempt attempt = task.
            getAttempt(TezTaskAttemptID.getInstance(task.getTaskID(), indexes.get(2)));
        if(attempt == null) {
          continue;
        }
        else {
          attempts.add(attempt);
        }

        if(attempts.size() >= limit) {
          break;
        }
      }
    }

    return attempts;
  }

  /**
   * Renders the response JSON for attemptsInfo API
   * The JSON will have an array of attempt objects under the key attempts.
   */
  public void getAttemptsInfo() {
    if (!setupResponse()) {
      return;
    }

    DAG dag = checkAndGetDAGFromRequest();
    if (dag == null) {
      return;
    }

    int limit = MAX_QUERIED;
    try {
      limit = getQueryParamInt(WebUIService.LIMIT);
    } catch (NumberFormatException e) {
      //Ignore
    }

    List<TaskAttempt> attempts = getRequestedAttempts(dag, limit);
    if(attempts == null) {
      return;
    }

    Map<String, Set<String>> counterNames = getCounterListFromRequest();

    ArrayList<Map<String, Object>> attemptsInfo = new ArrayList<Map<String, Object>>();
    for(TaskAttempt a : attempts) {
      Map<String, Object> attemptInfo = new HashMap<String, Object>();
      attemptInfo.put("id", a.getTaskAttemptID().toString());
      attemptInfo.put("progress", Float.toString(a.getProgress()));
      attemptInfo.put("status", a.getState().toString());

      try {
        TezCounters counters = a.getCounters();
        Map<String, Map<String, Long>> counterMap = constructCounterMapInfo(counters, counterNames);
        if (counterMap != null && !counterMap.isEmpty()) {
          attemptInfo.put("counters", counterMap);
        }
      } catch (LimitExceededException e) {
        // Ignore
        // TODO: add an error message instead for counter key
      }
      attemptsInfo.add(attemptInfo);
    }

    renderJSON(ImmutableMap.of("attempts", attemptsInfo));
  }

  @Override
  @VisibleForTesting
  public void renderJSON(Object object) {
    super.renderJSON(object);
  }

  public static class StaticAMView extends View {
    @Inject
    AppContext appContext;
    @Inject
    @Named("TezUIHistoryURL") String historyUrl;

    @Override
    public void render() {
      response().setContentType(MimeType.HTML);
      PrintWriter pw = writer();
      pw.write("<html>");
      pw.write("<head>");
      pw.write("<meta charset=\"utf-8\">");
      pw.write("<title>Redirecting to Tez UI</title>");
      pw.write("</head>");
      pw.write("<body>");
      if (historyUrl == null || historyUrl.isEmpty()) {
        pw.write("<h1>Tez UI Url is not defined.</h1>" +
            "<p>To enable tracking url pointing to Tez UI, set the config <b>" +
            TezConfiguration.TEZ_HISTORY_URL_BASE + "</b> in the tez-site.xml.</p>");
      } else {
        pw.write("<h1>Redirecting to Tez UI</h1>. <p>If you are not redirected shortly, click " +
            "<a href='" + historyUrl + "'><b>here</b></a></p>"
        );
        pw.write("<script type='text/javascript'>setTimeout(function() { " +
          "window.location.replace('" + historyUrl + "');" +
          "}, 0); </script>");
      }
      pw.write("</body>");
      pw.write("</html>");
      pw.flush();
    }
  }

  @VisibleForTesting
  static class ProgressInfo {
    private String id;

    public float getProgress() {
      return progress;
    }

    public String getId() {
      return id;
    }

    private float progress;

    public ProgressInfo(String id, float progress) {
      this.id = id;
      this.progress = progress;
    }
  }
}
