co.cask.cdap.gateway.handlers.ProgramLifecycleHttpHandler.java Source code

Java tutorial

Introduction

Here is the source code for co.cask.cdap.gateway.handlers.ProgramLifecycleHttpHandler.java

Source

/*
 * Copyright  2014-2016 Cask Data, Inc.
 *
 * Licensed 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 co.cask.cdap.gateway.handlers;

import co.cask.cdap.api.ProgramSpecification;
import co.cask.cdap.api.app.ApplicationSpecification;
import co.cask.cdap.api.flow.FlowSpecification;
import co.cask.cdap.api.flow.FlowletDefinition;
import co.cask.cdap.api.metrics.MetricStore;
import co.cask.cdap.api.schedule.ScheduleSpecification;
import co.cask.cdap.api.service.ServiceSpecification;
import co.cask.cdap.app.mapreduce.MRJobInfoFetcher;
import co.cask.cdap.app.runtime.ProgramController;
import co.cask.cdap.app.runtime.ProgramRuntimeService;
import co.cask.cdap.app.store.Store;
import co.cask.cdap.common.BadRequestException;
import co.cask.cdap.common.ConflictException;
import co.cask.cdap.common.MethodNotAllowedException;
import co.cask.cdap.common.NotFoundException;
import co.cask.cdap.common.NotImplementedException;
import co.cask.cdap.common.conf.Constants;
import co.cask.cdap.common.io.CaseInsensitiveEnumTypeAdapterFactory;
import co.cask.cdap.config.PreferencesStore;
import co.cask.cdap.data2.transaction.queue.QueueAdmin;
import co.cask.cdap.gateway.handlers.util.AbstractAppFabricHttpHandler;
import co.cask.cdap.internal.app.ApplicationSpecificationAdapter;
import co.cask.cdap.internal.app.runtime.flow.FlowUtils;
import co.cask.cdap.internal.app.runtime.schedule.Scheduler;
import co.cask.cdap.internal.app.runtime.schedule.SchedulerException;
import co.cask.cdap.internal.app.services.ProgramLifecycleService;
import co.cask.cdap.internal.app.store.RunRecordMeta;
import co.cask.cdap.proto.BatchProgram;
import co.cask.cdap.proto.BatchProgramResult;
import co.cask.cdap.proto.BatchProgramStart;
import co.cask.cdap.proto.BatchProgramStatus;
import co.cask.cdap.proto.BatchRunnable;
import co.cask.cdap.proto.BatchRunnableInstances;
import co.cask.cdap.proto.Containers;
import co.cask.cdap.proto.Id;
import co.cask.cdap.proto.Instances;
import co.cask.cdap.proto.MRJobInfo;
import co.cask.cdap.proto.NotRunningProgramLiveInfo;
import co.cask.cdap.proto.ProgramLiveInfo;
import co.cask.cdap.proto.ProgramRecord;
import co.cask.cdap.proto.ProgramRunStatus;
import co.cask.cdap.proto.ProgramStatus;
import co.cask.cdap.proto.ProgramType;
import co.cask.cdap.proto.RunRecord;
import co.cask.cdap.proto.ServiceInstances;
import co.cask.cdap.proto.id.Ids;
import co.cask.cdap.proto.id.NamespaceId;
import co.cask.cdap.proto.id.ProgramId;
import co.cask.cdap.proto.id.ProgramRunId;
import co.cask.http.HttpResponder;
import com.google.common.base.Charsets;
import com.google.common.base.Function;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.Lists;
import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.gson.JsonObject;
import com.google.gson.JsonSyntaxException;
import com.google.gson.reflect.TypeToken;
import com.google.inject.Inject;
import com.google.inject.Singleton;
import org.jboss.netty.buffer.ChannelBufferInputStream;
import org.jboss.netty.handler.codec.http.HttpRequest;
import org.jboss.netty.handler.codec.http.HttpResponseStatus;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.IOException;
import java.io.InputStreamReader;
import java.io.Reader;
import java.lang.reflect.Type;
import java.util.ArrayList;
import java.util.EnumSet;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.TimeUnit;
import javax.ws.rs.DELETE;
import javax.ws.rs.DefaultValue;
import javax.ws.rs.GET;
import javax.ws.rs.POST;
import javax.ws.rs.PUT;
import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
import javax.ws.rs.QueryParam;

/**
 * {@link co.cask.http.HttpHandler} to manage program lifecycle for v3 REST APIs
 */
@Singleton
@Path(Constants.Gateway.API_VERSION_3 + "/namespaces/{namespace-id}")
public class ProgramLifecycleHttpHandler extends AbstractAppFabricHttpHandler {
    private static final Logger LOG = LoggerFactory.getLogger(ProgramLifecycleHttpHandler.class);
    private static final Type BATCH_PROGRAMS_TYPE = new TypeToken<List<BatchProgram>>() {
    }.getType();
    private static final Type BATCH_RUNNABLES_TYPE = new TypeToken<List<BatchRunnable>>() {
    }.getType();
    private static final Type BATCH_STARTS_TYPE = new TypeToken<List<BatchProgramStart>>() {
    }.getType();
    /**
     * Json serializer/deserializer.
     */
    private static final Gson GSON = ApplicationSpecificationAdapter.addTypeAdapters(new GsonBuilder())
            .registerTypeAdapterFactory(new CaseInsensitiveEnumTypeAdapterFactory()).create();

    private static final Function<RunRecordMeta, RunRecord> CONVERT_TO_RUN_RECORD = new Function<RunRecordMeta, RunRecord>() {
        @Override
        public RunRecord apply(RunRecordMeta input) {
            return new RunRecord(input);
        }
    };

    private final ProgramLifecycleService lifecycleService;
    private final QueueAdmin queueAdmin;
    private final PreferencesStore preferencesStore;
    private final MetricStore metricStore;
    private final MRJobInfoFetcher mrJobInfoFetcher;

    /**
     * Store manages non-runtime lifecycle.
     */
    protected final Store store;

    /**
     * Runtime program service for running and managing programs.
     */
    protected final ProgramRuntimeService runtimeService;

    /**
     * Scheduler provides ability to schedule/un-schedule the jobs.
     */
    protected final Scheduler scheduler;

    @Inject
    ProgramLifecycleHttpHandler(Store store, ProgramRuntimeService runtimeService,
            ProgramLifecycleService lifecycleService, QueueAdmin queueAdmin, Scheduler scheduler,
            PreferencesStore preferencesStore, MRJobInfoFetcher mrJobInfoFetcher, MetricStore metricStore) {
        this.store = store;
        this.runtimeService = runtimeService;
        this.lifecycleService = lifecycleService;
        this.metricStore = metricStore;
        this.queueAdmin = queueAdmin;
        this.scheduler = scheduler;
        this.preferencesStore = preferencesStore;
        this.mrJobInfoFetcher = mrJobInfoFetcher;
    }

    /**
     * Relays job-level and task-level information about a particular MapReduce program run.
     */
    @GET
    @Path("/apps/{app-id}/mapreduce/{mapreduce-id}/runs/{run-id}/info")
    public void getMapReduceInfo(HttpRequest request, HttpResponder responder,
            @PathParam("namespace-id") String namespaceId, @PathParam("app-id") String appId,
            @PathParam("mapreduce-id") String mapreduceId, @PathParam("run-id") String runId)
            throws IOException, NotFoundException {
        Id.Program programId = Id.Program.from(namespaceId, appId, ProgramType.MAPREDUCE, mapreduceId);
        Id.Run run = new Id.Run(programId, runId);
        ApplicationSpecification appSpec = store.getApplication(programId.getApplication());
        if (appSpec == null) {
            throw new NotFoundException(programId.getApplication());
        }
        if (!appSpec.getMapReduce().containsKey(mapreduceId)) {
            throw new NotFoundException(programId);
        }
        RunRecordMeta runRecordMeta = store.getRun(programId, runId);
        if (runRecordMeta == null) {
            throw new NotFoundException(run);
        }

        MRJobInfo mrJobInfo = mrJobInfoFetcher.getMRJobInfo(run);

        mrJobInfo.setState(runRecordMeta.getStatus().name());
        // Multiple startTs / endTs by 1000, to be consistent with Task-level start/stop times returned by JobClient
        // in milliseconds. RunRecord returns seconds value.
        mrJobInfo.setStartTime(TimeUnit.SECONDS.toMillis(runRecordMeta.getStartTs()));
        Long stopTs = runRecordMeta.getStopTs();
        if (stopTs != null) {
            mrJobInfo.setStopTime(TimeUnit.SECONDS.toMillis(stopTs));
        }

        // JobClient (in DistributedMRJobInfoFetcher) can return NaN as some of the values, and GSON otherwise fails
        Gson gson = new GsonBuilder().serializeSpecialFloatingPointValues().create();
        responder.sendJson(HttpResponseStatus.OK, mrJobInfo, mrJobInfo.getClass(), gson);
    }

    /**
     * Returns status of a type specified by the type{flows,workflows,mapreduce,spark,services,schedules}.
     */
    @GET
    @Path("/apps/{app-id}/{type}/{id}/status")
    public void getStatus(HttpRequest request, HttpResponder responder,
            @PathParam("namespace-id") String namespaceId, @PathParam("app-id") String appId,
            @PathParam("type") String type, @PathParam("id") String id)
            throws NotFoundException, SchedulerException, BadRequestException {
        if (type.equals("schedules")) {
            getScheduleStatus(responder, appId, namespaceId, id);
            return;
        }

        ProgramType programType;
        try {
            programType = ProgramType.valueOfCategoryName(type);
        } catch (IllegalArgumentException e) {
            throw new BadRequestException(e);
        }
        ProgramId program = Ids.namespace(namespaceId).app(appId).program(programType, id);
        ProgramStatus programStatus = lifecycleService.getProgramStatus(program);

        Map<String, String> status = ImmutableMap.of("status", programStatus.name());
        responder.sendJson(HttpResponseStatus.OK, status);
    }

    private void getScheduleStatus(HttpResponder responder, String appId, String namespaceId, String scheduleName)
            throws NotFoundException, SchedulerException {
        Id.Application applicationId = Id.Application.from(namespaceId, appId);
        ApplicationSpecification appSpec = store.getApplication(applicationId);
        if (appSpec == null) {
            throw new NotFoundException(applicationId);
        }

        ScheduleSpecification scheduleSpec = appSpec.getSchedules().get(scheduleName);
        if (scheduleSpec == null) {
            throw new NotFoundException(scheduleName,
                    String.format("Schedule: %s for application: %s", scheduleName, applicationId.getId()));
        }

        String programName = scheduleSpec.getProgram().getProgramName();
        ProgramType programType = ProgramType.valueOfSchedulableType(scheduleSpec.getProgram().getProgramType());
        Id.Program programId = Id.Program.from(namespaceId, appId, programType, programName);
        JsonObject json = new JsonObject();
        json.addProperty("status", scheduler
                .scheduleState(programId, programId.getType().getSchedulableType(), scheduleName).toString());
        responder.sendJson(HttpResponseStatus.OK, json);
    }

    /**
     * Stops the particular run of the Workflow or MapReduce program.
     */
    @POST
    @Path("/apps/{app-id}/{type}/{id}/runs/{run-id}/stop")
    public void performRunLevelStop(HttpRequest request, HttpResponder responder,
            @PathParam("namespace-id") String namespaceId, @PathParam("app-id") String appId,
            @PathParam("type") String type, @PathParam("id") String id, @PathParam("run-id") String runId)
            throws Exception {
        ProgramType programType;
        try {
            programType = ProgramType.valueOfCategoryName(type);
        } catch (IllegalArgumentException e) {
            throw new BadRequestException(e);
        }
        ProgramId program = Ids.namespace(namespaceId).app(appId).program(programType, id);
        lifecycleService.stop(program, runId);
        responder.sendStatus(HttpResponseStatus.OK);
    }

    @POST
    @Path("/apps/{app-id}/{type}/{id}/{action}")
    public void performAction(HttpRequest request, HttpResponder responder,
            @PathParam("namespace-id") String namespaceId, @PathParam("app-id") String appId,
            @PathParam("type") String type, @PathParam("id") String id, @PathParam("action") String action)
            throws Exception {
        if ("schedules".equals(type)) {
            suspendResumeSchedule(responder, namespaceId, appId, id, action);
            return;
        }

        ProgramType programType;
        try {
            programType = ProgramType.valueOfCategoryName(type);
        } catch (IllegalArgumentException e) {
            throw new BadRequestException(String.format("Unknown program type '%s'", type), e);
        }

        ProgramId programId = Ids.namespace(namespaceId).app(appId).program(programType, id);
        Map<String, String> args = decodeArguments(request);
        // we have already validated that the action is valid
        switch (action.toLowerCase()) {
        case "start":
            lifecycleService.start(programId, args, false);
            break;
        case "debug":
            if (!isDebugAllowed(programType)) {
                throw new NotImplementedException(
                        String.format("debug action is not implemented for program type %s", programType));
            }
            lifecycleService.start(programId, args, true);
            break;
        case "stop":
            lifecycleService.stop(programId);
            break;
        default:
            throw new NotFoundException(String.format("%s action was not found", action));
        }
        responder.sendStatus(HttpResponseStatus.OK);
    }

    private void suspendResumeSchedule(HttpResponder responder, String namespaceId, String appId,
            String scheduleName, String action) throws SchedulerException {
        try {
            if (!action.equals("suspend") && !action.equals("resume")) {
                responder.sendString(HttpResponseStatus.BAD_REQUEST, "Schedule can only be suspended or resumed.");
                return;
            }

            ApplicationSpecification appSpec = store.getApplication(Id.Application.from(namespaceId, appId));
            if (appSpec == null) {
                responder.sendString(HttpResponseStatus.NOT_FOUND, "App: " + appId + " not found");
                return;
            }

            ScheduleSpecification scheduleSpec = appSpec.getSchedules().get(scheduleName);
            if (scheduleSpec == null) {
                responder.sendString(HttpResponseStatus.NOT_FOUND, "Schedule: " + scheduleName + " not found");
                return;
            }

            String programName = scheduleSpec.getProgram().getProgramName();
            ProgramType programType = ProgramType
                    .valueOfSchedulableType(scheduleSpec.getProgram().getProgramType());
            Id.Program programId = Id.Program.from(namespaceId, appId, programType, programName);
            Scheduler.ScheduleState state = scheduler.scheduleState(programId,
                    scheduleSpec.getProgram().getProgramType(), scheduleName);
            switch (state) {
            case NOT_FOUND:
                responder.sendStatus(HttpResponseStatus.NOT_FOUND);
                break;
            case SCHEDULED:
                if (action.equals("suspend")) {
                    scheduler.suspendSchedule(programId, scheduleSpec.getProgram().getProgramType(), scheduleName);
                    responder.sendJson(HttpResponseStatus.OK, "OK");
                } else {
                    // attempt to resume already resumed schedule
                    responder.sendJson(HttpResponseStatus.CONFLICT, "Already resumed");
                }
                break;
            case SUSPENDED:
                if (action.equals("suspend")) {
                    // attempt to suspend already suspended schedule
                    responder.sendJson(HttpResponseStatus.CONFLICT, "Schedule already suspended");
                } else {
                    scheduler.resumeSchedule(programId, scheduleSpec.getProgram().getProgramType(), scheduleName);
                    responder.sendJson(HttpResponseStatus.OK, "OK");
                }
                break;
            }
        } catch (SecurityException e) {
            responder.sendStatus(HttpResponseStatus.UNAUTHORIZED);
        } catch (NotFoundException e) {
            responder.sendString(HttpResponseStatus.NOT_FOUND, e.getMessage());
        }
    }

    /**
     * Returns program runs based on options it returns either currently running or completed or failed.
     * Default it returns all.
     */
    @GET
    @Path("/apps/{app-id}/{program-type}/{program-id}/runs")
    public void programHistory(HttpRequest request, HttpResponder responder,
            @PathParam("namespace-id") String namespaceId, @PathParam("app-id") String appId,
            @PathParam("program-type") String programType, @PathParam("program-id") String programId,
            @QueryParam("status") String status, @QueryParam("start") String startTs,
            @QueryParam("end") String endTs, @QueryParam("limit") @DefaultValue("100") final int resultLimit)
            throws BadRequestException, NotFoundException {
        ProgramType type = getProgramType(programType);
        if (type == null || type == ProgramType.WEBAPP) {
            throw new NotFoundException(
                    String.format("Program history is not supported for program type '%s'.", programType));
        }
        long start = (startTs == null || startTs.isEmpty()) ? 0 : Long.parseLong(startTs);
        long end = (endTs == null || endTs.isEmpty()) ? Long.MAX_VALUE : Long.parseLong(endTs);
        getRuns(responder, Id.Program.from(namespaceId, appId, type, programId), status, start, end, resultLimit);
    }

    /**
     * Returns run record for a particular run of a program.
     */
    @GET
    @Path("/apps/{app-id}/{program-type}/{program-id}/runs/{run-id}")
    public void programRunRecord(HttpRequest request, HttpResponder responder,
            @PathParam("namespace-id") String namespaceId, @PathParam("app-id") String appId,
            @PathParam("program-type") String programType, @PathParam("program-id") String programId,
            @PathParam("run-id") String runid) throws NotFoundException {
        ProgramType type = getProgramType(programType);
        if (type == null || type == ProgramType.WEBAPP) {
            throw new NotFoundException(
                    String.format("Program run record is not supported for program type '%s'.", programType));
        }
        Id.Program progId = Id.Program.from(namespaceId, appId, type, programId);
        RunRecordMeta runRecordMeta = store.getRun(progId, runid);
        if (runRecordMeta != null) {
            RunRecord runRecord = CONVERT_TO_RUN_RECORD.apply(runRecordMeta);
            responder.sendJson(HttpResponseStatus.OK, runRecord);
            return;
        }
        throw new NotFoundException(new ProgramRunId(namespaceId, appId, type, programId, runid));
    }

    /**
     * Get program runtime args.
     */
    @GET
    @Path("/apps/{app-id}/{program-type}/{program-id}/runtimeargs")
    public void getProgramRuntimeArgs(HttpRequest request, HttpResponder responder,
            @PathParam("namespace-id") String namespaceId, @PathParam("app-id") String appId,
            @PathParam("program-type") String programType, @PathParam("program-id") String programId)
            throws BadRequestException, NotImplementedException, NotFoundException {
        ProgramType type = getProgramType(programType);
        if (type == null || type == ProgramType.WEBAPP) {
            throw new NotFoundException(String.format(
                    "Getting program runtime arguments is not supported for program " + "type '%s'.", programType));
        }

        Id.Program id = Id.Program.from(namespaceId, appId, type, programId);
        if (!store.programExists(id)) {
            throw new NotFoundException(id);
        }

        Map<String, String> runtimeArgs = preferencesStore.getProperties(id.getNamespaceId(), appId, programType,
                programId);
        responder.sendJson(HttpResponseStatus.OK, runtimeArgs);
    }

    /**
     * Save program runtime args.
     */
    @PUT
    @Path("/apps/{app-id}/{program-type}/{program-id}/runtimeargs")
    public void saveProgramRuntimeArgs(HttpRequest request, HttpResponder responder,
            @PathParam("namespace-id") String namespaceId, @PathParam("app-id") String appId,
            @PathParam("program-type") String programType, @PathParam("program-id") String programId)
            throws Exception {
        ProgramType type = getProgramType(programType);
        if (type == null || type == ProgramType.WEBAPP) {
            throw new NotFoundException(String.format(
                    "Saving program runtime arguments is not supported for program " + "type '%s'.", programType));
        }

        lifecycleService.saveRuntimeArgs(Ids.namespace(namespaceId).app(appId).program(type, programId),
                decodeArguments(request));
        responder.sendStatus(HttpResponseStatus.OK);
    }

    @GET
    @Path("/apps/{app-id}/{program-type}/{program-id}")
    public void programSpecification(HttpRequest request, HttpResponder responder,
            @PathParam("namespace-id") String namespaceId, @PathParam("app-id") String appId,
            @PathParam("program-type") String programType, @PathParam("program-id") String programId)
            throws Exception {
        ProgramType type = getProgramType(programType);
        if (type == null) {
            throw new MethodNotAllowedException(request.getMethod(), request.getUri());
        }

        ProgramId id = Ids.namespace(namespaceId).app(appId).program(type, programId);
        ProgramSpecification specification = lifecycleService.getProgramSpecification(id);
        if (specification == null) {
            throw new NotFoundException(programId);
        }
        responder.sendJson(HttpResponseStatus.OK, specification);
    }

    /**
     * Returns the status for all programs that are passed into the data. The data is an array of JSON objects
     * where each object must contain the following three elements: appId, programType, and programId
     * (flow name, service name, etc.).
     * <p>
     * Example input:
     * <pre><code>
     * [{"appId": "App1", "programType": "Service", "programId": "Service1"},
     * {"appId": "App1", "programType": "Mapreduce", "programId": "MapReduce2"},
     * {"appId": "App2", "programType": "Flow", "programId": "Flow1"}]
     * </code></pre>
     * </p><p>
     * The response will be an array of JsonObjects each of which will contain the three input parameters
     * as well as 2 fields, "status" which maps to the status of the program and "statusCode" which maps to the
     * status code for the data in that JsonObjects.
     * </p><p>
     * If an error occurs in the input (for the example above, App2 does not exist), then all JsonObjects for which the
     * parameters have a valid status will have the status field but all JsonObjects for which the parameters do not have
     * a valid status will have an error message and statusCode.
     * </p><p>
     * For example, if there is no App2 in the data above, then the response would be 200 OK with following possible data:
     * </p>
     * <pre><code>
     * [{"appId": "App1", "programType": "Service", "programId": "Service1", "statusCode": 200, "status": "RUNNING"},
     * {"appId": "App1", "programType": "Mapreduce", "programId": "Mapreduce2", "statusCode": 200, "status": "STOPPED"},
     * {"appId":"App2", "programType":"Flow", "programId":"Flow1", "statusCode":404, "error": "App: App2 not found"}]
     * </code></pre>
     */
    @POST
    @Path("/status")
    public void getStatuses(HttpRequest request, HttpResponder responder,
            @PathParam("namespace-id") String namespaceId) throws IOException, BadRequestException {

        List<BatchProgram> programs = validateAndGetBatchInput(request, BATCH_PROGRAMS_TYPE);

        List<BatchProgramStatus> statuses = new ArrayList<>(programs.size());
        for (BatchProgram program : programs) {
            ProgramId programId = Ids.namespace(namespaceId).app(program.getAppId())
                    .program(program.getProgramType(), program.getProgramId());
            try {
                ProgramStatus programStatus = lifecycleService.getProgramStatus(programId);
                statuses.add(new BatchProgramStatus(program, HttpResponseStatus.OK.getCode(), null,
                        programStatus.name()));
            } catch (NotFoundException e) {
                statuses.add(new BatchProgramStatus(program, HttpResponseStatus.NOT_FOUND.getCode(), e.getMessage(),
                        null));
            }
        }
        responder.sendJson(HttpResponseStatus.OK, statuses);
    }

    /**
     * Stops all programs that are passed into the data. The data is an array of JSON objects
     * where each object must contain the following three elements: appId, programType, and programId
     * (flow name, service name, etc.).
     * <p>
     * Example input:
     * <pre><code>
     * [{"appId": "App1", "programType": "Service", "programId": "Service1"},
     * {"appId": "App1", "programType": "Mapreduce", "programId": "MapReduce2"},
     * {"appId": "App2", "programType": "Flow", "programId": "Flow1"}]
     * </code></pre>
     * </p><p>
     * The response will be an array of JsonObjects each of which will contain the three input parameters
     * as well as a "statusCode" field which maps to the status code for the data in that JsonObjects.
     * </p><p>
     * If an error occurs in the input (for the example above, App2 does not exist), then all JsonObjects for which the
     * parameters have a valid status will have the status field but all JsonObjects for which the parameters do not have
     * a valid status will have an error message and statusCode.
     * </p><p>
     * For example, if there is no App2 in the data above, then the response would be 200 OK with following possible data:
     * </p>
     * <pre><code>
     * [{"appId": "App1", "programType": "Service", "programId": "Service1", "statusCode": 200},
     * {"appId": "App1", "programType": "Mapreduce", "programId": "Mapreduce2", "statusCode": 200},
     * {"appId":"App2", "programType":"Flow", "programId":"Flow1", "statusCode":404, "error": "App: App2 not found"}]
     * </code></pre>
     */
    @POST
    @Path("/stop")
    public void stopPrograms(HttpRequest request, HttpResponder responder,
            @PathParam("namespace-id") String namespaceId) throws Exception {

        List<BatchProgram> programs = validateAndGetBatchInput(request, BATCH_PROGRAMS_TYPE);

        List<ListenableFuture<BatchProgramResult>> issuedStops = new ArrayList<>(programs.size());
        for (final BatchProgram program : programs) {
            ProgramId programId = Ids.namespace(namespaceId).app(program.getAppId())
                    .program(program.getProgramType(), program.getProgramId());
            try {
                ListenableFuture<BatchProgramResult> issuedStop = Futures.transform(
                        lifecycleService.issueStop(programId, null),
                        new Function<ProgramController, BatchProgramResult>() {
                            @Override
                            public BatchProgramResult apply(ProgramController input) {
                                return new BatchProgramResult(program, HttpResponseStatus.OK.getCode(), null);
                            }
                        });
                issuedStops.add(issuedStop);
            } catch (NotFoundException e) {
                issuedStops.add(Futures.immediateFuture(
                        new BatchProgramResult(program, HttpResponseStatus.NOT_FOUND.getCode(), e.getMessage())));
            } catch (BadRequestException e) {
                issuedStops.add(Futures.immediateFuture(
                        new BatchProgramResult(program, HttpResponseStatus.BAD_REQUEST.getCode(), e.getMessage())));
            }
        }

        List<BatchProgramResult> output = new ArrayList<>(programs.size());
        // need to keep this index in case there is an exception getting the future, since we won't have the program
        // information in that scenario
        int i = 0;
        for (ListenableFuture<BatchProgramResult> issuedStop : issuedStops) {
            try {
                output.add(issuedStop.get());
            } catch (Throwable t) {
                LOG.warn(t.getMessage(), t);
                output.add(new BatchProgramResult(programs.get(i),
                        HttpResponseStatus.INTERNAL_SERVER_ERROR.getCode(), t.getMessage()));
            }
            i++;
        }
        responder.sendJson(HttpResponseStatus.OK, output);
    }

    /**
     * Starts all programs that are passed into the data. The data is an array of JSON objects
     * where each object must contain the following three elements: appId, programType, and programId
     * (flow name, service name, etc.). In additional, each object can contain an optional runtimeargs element,
     * which is a map of arguments to start the program with.
     * <p>
     * Example input:
     * <pre><code>
     * [{"appId": "App1", "programType": "Service", "programId": "Service1"},
     * {"appId": "App1", "programType": "Mapreduce", "programId": "MapReduce2", "runtimeargs":{"arg1":"val1"}},
     * {"appId": "App2", "programType": "Flow", "programId": "Flow1"}]
     * </code></pre>
     * </p><p>
     * The response will be an array of JsonObjects each of which will contain the three input parameters
     * as well as a "statusCode" field which maps to the status code for the data in that JsonObjects.
     * </p><p>
     * If an error occurs in the input (for the example above, App2 does not exist), then all JsonObjects for which the
     * parameters have a valid status will have the status field but all JsonObjects for which the parameters do not have
     * a valid status will have an error message and statusCode.
     * </p><p>
     * For example, if there is no App2 in the data above, then the response would be 200 OK with following possible data:
     * </p>
     * <pre><code>
     * [{"appId": "App1", "programType": "Service", "programId": "Service1", "statusCode": 200},
     * {"appId": "App1", "programType": "Mapreduce", "programId": "Mapreduce2", "statusCode": 200},
     * {"appId":"App2", "programType":"Flow", "programId":"Flow1", "statusCode":404, "error": "App: App2 not found"}]
     * </code></pre>
     */
    @POST
    @Path("/start")
    public void startPrograms(HttpRequest request, HttpResponder responder,
            @PathParam("namespace-id") String namespaceId) throws Exception {

        List<BatchProgramStart> programs = validateAndGetBatchInput(request, BATCH_STARTS_TYPE);

        List<BatchProgramResult> output = new ArrayList<>(programs.size());
        for (BatchProgramStart program : programs) {
            ProgramId programId = Ids.namespace(namespaceId).app(program.getAppId())
                    .program(program.getProgramType(), program.getProgramId());
            try {
                lifecycleService.start(programId, program.getRuntimeargs(), false);
                output.add(new BatchProgramResult(program, HttpResponseStatus.OK.getCode(), null));
            } catch (NotFoundException e) {
                output.add(new BatchProgramResult(program, HttpResponseStatus.NOT_FOUND.getCode(), e.getMessage()));
            } catch (BadRequestException e) {
                output.add(
                        new BatchProgramResult(program, HttpResponseStatus.BAD_REQUEST.getCode(), e.getMessage()));
            } catch (ConflictException e) {
                output.add(new BatchProgramResult(program, HttpResponseStatus.CONFLICT.getCode(), e.getMessage()));
            }
        }
        responder.sendJson(HttpResponseStatus.OK, output);
    }

    /**
     * Returns the number of instances for all program runnables that are passed into the data. The data is an array of
     * Json objects where each object must contain the following three elements: appId, programType, and programId
     * (flow name, service name). Retrieving instances only applies to flows, and user
     * services. For flows, another parameter, "runnableId", must be provided. This corresponds to the
     * flowlet/runnable for which to retrieve the instances.
     * <p>
     * Example input:
     * <pre><code>
     * [{"appId": "App1", "programType": "Service", "programId": "Service1", "runnableId": "Runnable1"},
     *  {"appId": "App1", "programType": "Mapreduce", "programId": "Mapreduce2"},
     *  {"appId": "App2", "programType": "Flow", "programId": "Flow1", "runnableId": "Flowlet1"}]
     * </code></pre>
     * </p><p>
     * The response will be an array of JsonObjects each of which will contain the three input parameters
     * as well as 3 fields:
     * <ul>
     * <li>"provisioned" which maps to the number of instances actually provided for the input runnable;</li>
     * <li>"requested" which maps to the number of instances the user has requested for the input runnable; and</li>
     * <li>"statusCode" which maps to the http status code for the data in that JsonObjects (200, 400, 404).</li>
     * </ul>
     * </p><p>
     * If an error occurs in the input (for the example above, Flowlet1 does not exist), then all JsonObjects for
     * which the parameters have a valid instances will have the provisioned and requested fields status code fields
     * but all JsonObjects for which the parameters are not valid will have an error message and statusCode.
     * </p><p>
     * For example, if there is no Flowlet1 in the above data, then the response could be 200 OK with the following data:
     * </p>
     * <pre><code>
     * [{"appId": "App1", "programType": "Service", "programId": "Service1", "runnableId": "Runnable1",
     *   "statusCode": 200, "provisioned": 2, "requested": 2},
     *  {"appId": "App1", "programType": "Mapreduce", "programId": "Mapreduce2", "statusCode": 400,
     *   "error": "Program type 'Mapreduce' is not a valid program type to get instances"},
     *  {"appId": "App2", "programType": "Flow", "programId": "Flow1", "runnableId": "Flowlet1", "statusCode": 404,
     *   "error": "Program": Flowlet1 not found"}]
     * </code></pre>
     */
    @POST
    @Path("/instances")
    public void getInstances(HttpRequest request, HttpResponder responder,
            @PathParam("namespace-id") String namespaceId) throws IOException, BadRequestException {

        List<BatchRunnable> runnables = validateAndGetBatchInput(request, BATCH_RUNNABLES_TYPE);

        // cache app specs to perform fewer store lookups
        Map<Id.Application, ApplicationSpecification> appSpecs = new HashMap<>();

        List<BatchRunnableInstances> output = new ArrayList<>(runnables.size());
        for (BatchRunnable runnable : runnables) {
            // cant get instances for things that are not flows, services, or workers
            if (!canHaveInstances(runnable.getProgramType())) {
                output.add(new BatchRunnableInstances(runnable, HttpResponseStatus.BAD_REQUEST.getCode(),
                        String.format("Program type '%s' is not a valid program type to get instances",
                                runnable.getProgramType().getPrettyName())));
                continue;
            }

            Id.Application appId = Id.Application.from(namespaceId, runnable.getAppId());

            // populate spec cache if this is the first time we've seen the appid.
            if (!appSpecs.containsKey(appId)) {
                appSpecs.put(appId, store.getApplication(appId));
            }

            ApplicationSpecification spec = appSpecs.get(appId);
            if (spec == null) {
                output.add(new BatchRunnableInstances(runnable, HttpResponseStatus.NOT_FOUND.getCode(),
                        String.format("App: %s not found", appId)));
                continue;
            }

            Id.Program programId = Id.Program.from(appId, runnable.getProgramType(), runnable.getProgramId());
            output.add(getProgramInstances(runnable, spec, programId));
        }
        responder.sendJson(HttpResponseStatus.OK, output);
    }

    /*
    Note: Cannot combine the following get all programs methods into one because then API path will clash with /apps path
     */

    /**
     * Returns a list of flows associated with a namespace.
     */
    @GET
    @Path("/flows")
    public void getAllFlows(HttpRequest request, HttpResponder responder,
            @PathParam("namespace-id") String namespaceId) throws Exception {
        programList(responder, namespaceId, ProgramType.FLOW, store);
    }

    /**
     * Returns a list of map/reduces associated with a namespace.
     */
    @GET
    @Path("/mapreduce")
    public void getAllMapReduce(HttpRequest request, HttpResponder responder,
            @PathParam("namespace-id") String namespaceId) throws Exception {
        programList(responder, namespaceId, ProgramType.MAPREDUCE, store);
    }

    /**
     * Returns a list of spark jobs associated with a namespace.
     */
    @GET
    @Path("/spark")
    public void getAllSpark(HttpRequest request, HttpResponder responder,
            @PathParam("namespace-id") String namespaceId) throws Exception {
        programList(responder, namespaceId, ProgramType.SPARK, store);
    }

    /**
     * Returns a list of workflows associated with a namespace.
     */
    @GET
    @Path("/workflows")
    public void getAllWorkflows(HttpRequest request, HttpResponder responder,
            @PathParam("namespace-id") String namespaceId) throws Exception {
        programList(responder, namespaceId, ProgramType.WORKFLOW, store);
    }

    /**
     * Returns a list of services associated with a namespace.
     */
    @GET
    @Path("/services")
    public void getAllServices(HttpRequest request, HttpResponder responder,
            @PathParam("namespace-id") String namespaceId) throws Exception {
        programList(responder, namespaceId, ProgramType.SERVICE, store);
    }

    @GET
    @Path("/workers")
    public void getAllWorkers(HttpRequest request, HttpResponder responder,
            @PathParam("namespace-id") String namespaceId) throws Exception {
        programList(responder, namespaceId, ProgramType.WORKER, store);
    }

    /**
     * Returns number of instances of a worker.
     */
    @GET
    @Path("/apps/{app-id}/workers/{worker-id}/instances")
    public void getWorkerInstances(HttpRequest request, HttpResponder responder,
            @PathParam("namespace-id") String namespaceId, @PathParam("app-id") String appId,
            @PathParam("worker-id") String workerId) {
        try {
            int count = store.getWorkerInstances(Id.Program.from(namespaceId, appId, ProgramType.WORKER, workerId));
            responder.sendJson(HttpResponseStatus.OK, new Instances(count));
        } catch (SecurityException e) {
            responder.sendStatus(HttpResponseStatus.UNAUTHORIZED);
        } catch (Throwable e) {
            if (respondIfElementNotFound(e, responder)) {
                return;
            }
            throw e;
        }
    }

    /**
     * Sets the number of instances of a worker.
     */
    @PUT
    @Path("/apps/{app-id}/workers/{worker-id}/instances")
    public void setWorkerInstances(HttpRequest request, HttpResponder responder,
            @PathParam("namespace-id") String namespaceId, @PathParam("app-id") String appId,
            @PathParam("worker-id") String workerId) throws Exception {
        int instances = getInstances(request);
        try {
            lifecycleService.setInstances(Ids.namespace(namespaceId).app(appId).worker(workerId), instances);
            responder.sendStatus(HttpResponseStatus.OK);
        } catch (SecurityException e) {
            responder.sendStatus(HttpResponseStatus.UNAUTHORIZED);
        } catch (Throwable e) {
            if (respondIfElementNotFound(e, responder)) {
                return;
            }
            throw e;
        }
    }

    /********************** Flow/Flowlet APIs ***********************************************************/
    /**
     * Returns number of instances for a flowlet within a flow.
     */
    @GET
    @Path("/apps/{app-id}/flows/{flow-id}/flowlets/{flowlet-id}/instances")
    public void getFlowletInstances(HttpRequest request, HttpResponder responder,
            @PathParam("namespace-id") String namespaceId, @PathParam("app-id") String appId,
            @PathParam("flow-id") String flowId, @PathParam("flowlet-id") String flowletId) {
        try {
            int count = store.getFlowletInstances(Id.Program.from(namespaceId, appId, ProgramType.FLOW, flowId),
                    flowletId);
            responder.sendJson(HttpResponseStatus.OK, new Instances(count));
        } catch (SecurityException e) {
            responder.sendStatus(HttpResponseStatus.UNAUTHORIZED);
        } catch (Throwable e) {
            if (respondIfElementNotFound(e, responder)) {
                return;
            }
            throw e;
        }
    }

    /**
     * Increases number of instance for a flowlet within a flow.
     */
    @PUT
    @Path("/apps/{app-id}/flows/{flow-id}/flowlets/{flowlet-id}/instances")
    public synchronized void setFlowletInstances(HttpRequest request, HttpResponder responder,
            @PathParam("namespace-id") String namespaceId, @PathParam("app-id") String appId,
            @PathParam("flow-id") String flowId, @PathParam("flowlet-id") String flowletId) throws Exception {
        int instances = getInstances(request);
        try {
            lifecycleService.setInstances(Ids.namespace(namespaceId).app(appId).flow(flowId), instances, flowletId);
            responder.sendStatus(HttpResponseStatus.OK);
        } catch (SecurityException e) {
            responder.sendStatus(HttpResponseStatus.UNAUTHORIZED);
        } catch (Throwable e) {
            if (respondIfElementNotFound(e, responder)) {
                return;
            }
            throw e;
        }
    }

    @GET
    @Path("/apps/{app-id}/{program-category}/{program-id}/live-info")
    @SuppressWarnings("unused")
    public void liveInfo(HttpRequest request, HttpResponder responder,
            @PathParam("namespace-id") String namespaceId, @PathParam("app-id") String appId,
            @PathParam("program-category") String programCategory, @PathParam("program-id") String programId) {
        ProgramType type = getProgramType(programCategory);
        if (type == null) {
            responder.sendString(HttpResponseStatus.METHOD_NOT_ALLOWED,
                    String.format("Live-info not supported for program type '%s'", programCategory));
            return;
        }
        Id.Program program = Id.Program.from(namespaceId, appId, ProgramType.valueOfCategoryName(programCategory),
                programId);
        getLiveInfo(responder, program, runtimeService);
    }

    /**
     * Deletes queues.
     */
    @DELETE
    @Path("/apps/{app-id}/flows/{flow-id}/queues")
    public void deleteFlowQueues(HttpRequest request, HttpResponder responder,
            @PathParam("namespace-id") String namespaceId, @PathParam("app-id") String appId,
            @PathParam("flow-id") String flowId) throws Exception {
        ProgramId programId = Ids.namespace(namespaceId).app(appId).flow(flowId);
        try {
            ProgramStatus status = lifecycleService.getProgramStatus(programId);
            if (ProgramStatus.RUNNING == status) {
                responder.sendString(HttpResponseStatus.FORBIDDEN, "Flow is running, please stop it first.");
            } else {
                queueAdmin.dropAllForFlow(Id.Flow.from(programId.getApplication(), programId.getProgram()));
                FlowUtils.deleteFlowPendingMetrics(metricStore, namespaceId, appId, flowId);
                responder.sendStatus(HttpResponseStatus.OK);
            }
        } catch (SecurityException e) {
            responder.sendStatus(HttpResponseStatus.UNAUTHORIZED);
        }
    }

    /**
     * Return the number of instances of a service.
     */
    @GET
    @Path("/apps/{app-id}/services/{service-id}/instances")
    public void getServiceInstances(HttpRequest request, HttpResponder responder,
            @PathParam("namespace-id") String namespaceId, @PathParam("app-id") String appId,
            @PathParam("service-id") String serviceId) {
        try {
            ProgramId programId = Ids.namespace(namespaceId).app(appId).service(serviceId);
            if (!store.programExists(programId.toId())) {
                responder.sendString(HttpResponseStatus.NOT_FOUND, "Service not found");
                return;
            }

            ServiceSpecification specification = (ServiceSpecification) lifecycleService
                    .getProgramSpecification(programId);
            if (specification == null) {
                responder.sendStatus(HttpResponseStatus.NOT_FOUND);
                return;
            }

            int instances = specification.getInstances();
            responder.sendJson(HttpResponseStatus.OK,
                    new ServiceInstances(instances, getInstanceCount(programId, serviceId)));
        } catch (SecurityException e) {
            responder.sendStatus(HttpResponseStatus.UNAUTHORIZED);
        }
    }

    /**
     * Set instances of a service.
     */
    @PUT
    @Path("/apps/{app-id}/services/{service-id}/instances")
    public void setServiceInstances(HttpRequest request, HttpResponder responder,
            @PathParam("namespace-id") String namespaceId, @PathParam("app-id") String appId,
            @PathParam("service-id") String serviceId) throws Exception {
        try {
            ProgramId programId = Ids.namespace(namespaceId).app(appId).service(serviceId);
            if (!store.programExists(programId.toId())) {
                responder.sendString(HttpResponseStatus.NOT_FOUND, "Service not found");
                return;
            }

            int instances = getInstances(request);
            lifecycleService.setInstances(programId, instances);
            responder.sendStatus(HttpResponseStatus.OK);
        } catch (SecurityException e) {
            responder.sendStatus(HttpResponseStatus.UNAUTHORIZED);
        } catch (Throwable throwable) {
            if (respondIfElementNotFound(throwable, responder)) {
                return;
            }
            throw throwable;
        }
    }

    @DELETE
    @Path("/queues")
    public synchronized void deleteQueues(HttpRequest request, HttpResponder responder,
            @PathParam("namespace-id") String namespaceId) {
        // synchronized to avoid a potential race condition here:
        // 1. the check for state returns that all flows are STOPPED
        // 2. The API deletes queues because
        // Between 1. and 2., a flow is started using the /namespaces/{namespace-id}/apps/{app-id}/flows/{flow-id}/start API
        // Averting this race condition by synchronizing this method. The resource that needs to be locked here is
        // runtimeService. This should work because the method that is used to start a flow - startStopProgram - is also
        // synchronized on this.
        // This synchronization works in HA mode because even in HA mode there is only one leader at a time.
        NamespaceId namespace = Ids.namespace(namespaceId);
        try {
            List<ProgramRecord> flows = listPrograms(namespace, ProgramType.FLOW, store);
            for (ProgramRecord flow : flows) {
                String appId = flow.getApp();
                String flowId = flow.getName();
                ProgramId programId = Ids.namespace(namespaceId).app(appId).flow(flowId);
                ProgramStatus status = lifecycleService.getProgramStatus(programId);
                if (ProgramStatus.STOPPED != status) {
                    responder.sendString(HttpResponseStatus.FORBIDDEN,
                            String.format("Flow '%s' from application '%s' in namespace '%s' is running, "
                                    + "please stop it first.", flowId, appId, namespaceId));
                    return;
                }
            }
            queueAdmin.dropAllInNamespace(namespace.toId());
            // delete process metrics that are used to calculate the queue size (system.queue.pending metric)
            FlowUtils.deleteFlowPendingMetrics(metricStore, namespaceId, null, null);
            responder.sendStatus(HttpResponseStatus.OK);
        } catch (Exception e) {
            LOG.error("Error while deleting queues in namespace " + namespace, e);
            responder.sendString(HttpResponseStatus.INTERNAL_SERVER_ERROR, e.getMessage());
        }
    }

    /**
     * Get requested and provisioned instances for a program type.
     * The program type passed here should be one that can have instances (flows, services, ...)
     * Requires caller to do this validation.
     */
    private BatchRunnableInstances getProgramInstances(BatchRunnable runnable, ApplicationSpecification spec,
            Id.Program programId) {
        int requested;
        String programName = programId.getId();
        String runnableId = programName;
        ProgramType programType = programId.getType();
        if (programType == ProgramType.WORKER) {
            if (!spec.getWorkers().containsKey(programName)) {
                return new BatchRunnableInstances(runnable, HttpResponseStatus.NOT_FOUND.getCode(),
                        "Worker: " + programName + " not found");
            }
            requested = spec.getWorkers().get(programName).getInstances();

        } else if (programType == ProgramType.SERVICE) {
            if (!spec.getServices().containsKey(programName)) {
                return new BatchRunnableInstances(runnable, HttpResponseStatus.NOT_FOUND.getCode(),
                        "Service: " + programName + " not found");
            }
            requested = spec.getServices().get(programName).getInstances();

        } else if (programType == ProgramType.FLOW) {
            // flows must have runnable id
            runnableId = runnable.getRunnableId();
            if (runnableId == null) {
                return new BatchRunnableInstances(runnable, HttpResponseStatus.BAD_REQUEST.getCode(),
                        "Must provide the flowlet id as the runnableId for flows");
            }
            FlowSpecification flowSpec = spec.getFlows().get(programName);
            if (flowSpec == null) {
                return new BatchRunnableInstances(runnable, HttpResponseStatus.NOT_FOUND.getCode(),
                        "Flow: " + programName + " not found");
            }
            FlowletDefinition flowletDefinition = flowSpec.getFlowlets().get(runnableId);
            if (flowletDefinition == null) {
                return new BatchRunnableInstances(runnable, HttpResponseStatus.NOT_FOUND.getCode(),
                        "Flowlet: " + runnableId + " not found");
            }
            requested = flowletDefinition.getInstances();

        } else {
            return new BatchRunnableInstances(runnable, HttpResponseStatus.BAD_REQUEST.getCode(),
                    "Instances not supported for program type + " + programType);
        }
        int provisioned = getInstanceCount(programId.toEntityId(), runnableId);
        // use the pretty name of program types to be consistent
        return new BatchRunnableInstances(runnable, HttpResponseStatus.OK.getCode(), provisioned, requested);
    }

    private void getRuns(HttpResponder responder, Id.Program programId, String status, long start, long end,
            int limit) throws BadRequestException {
        try {
            ProgramRunStatus runStatus = (status == null) ? ProgramRunStatus.ALL
                    : ProgramRunStatus.valueOf(status.toUpperCase());
            List<RunRecord> records = Lists.transform(store.getRuns(programId, runStatus, start, end, limit),
                    CONVERT_TO_RUN_RECORD);
            responder.sendJson(HttpResponseStatus.OK, records);
        } catch (IllegalArgumentException e) {
            throw new BadRequestException(String.format(
                    "Invalid status %s. Supported options for status of runs are " + "running/completed/failed",
                    status));
        }
    }

    /**
     * Returns the number of instances currently running for different runnables for different programs
     */
    private int getInstanceCount(ProgramId programId, String runnableId) {
        ProgramLiveInfo info = runtimeService.getLiveInfo(programId.toId());
        int count = 0;
        if (info instanceof NotRunningProgramLiveInfo) {
            return count;
        }
        if (info instanceof Containers) {
            Containers containers = (Containers) info;
            for (Containers.ContainerInfo container : containers.getContainers()) {
                if (container.getName().equals(runnableId)) {
                    count++;
                }
            }
            return count;
        }
        // TODO: CDAP-1091: For standalone mode, returning the requested instances instead of provisioned only for services.
        // Doing this only for services to keep it consistent with the existing contract for flowlets right now.
        // The get instances contract for both flowlets and services should be re-thought and fixed as part of CDAP-1091
        if (programId.getType() == ProgramType.SERVICE) {
            return getRequestedServiceInstances(programId.toId());
        }

        // Not running on YARN default 1
        return 1;
    }

    private int getRequestedServiceInstances(Id.Program serviceId) {
        // Not running on YARN, get it from store
        return store.getServiceInstances(serviceId);
    }

    private boolean isDebugAllowed(ProgramType programType) {
        return EnumSet.of(ProgramType.FLOW, ProgramType.SERVICE, ProgramType.WORKER).contains(programType);
    }

    private boolean canHaveInstances(ProgramType programType) {
        return EnumSet.of(ProgramType.FLOW, ProgramType.SERVICE, ProgramType.WORKER).contains(programType);
    }

    private <T extends BatchProgram> List<T> validateAndGetBatchInput(HttpRequest request, Type type)
            throws BadRequestException, IOException {

        List<T> programs;
        try (Reader reader = new InputStreamReader(new ChannelBufferInputStream(request.getContent()),
                Charsets.UTF_8)) {
            try {
                programs = GSON.fromJson(reader, type);
                if (programs == null) {
                    throw new BadRequestException(
                            "Request body is invalid json, please check that it is a json array.");
                }
            } catch (JsonSyntaxException e) {
                throw new BadRequestException("Request body is invalid json: " + e.getMessage());
            }
        }

        // validate input
        for (BatchProgram program : programs) {
            try {
                program.validate();
            } catch (IllegalArgumentException e) {
                throw new BadRequestException(
                        "Must provide valid appId, programType, and programId for each object: " + e.getMessage());
            }
        }
        return programs;
    }
}