org.venice.beachfront.bfapi.services.PiazzaService.java Source code

Java tutorial

Introduction

Here is the source code for org.venice.beachfront.bfapi.services.PiazzaService.java

Source

/**
 * Copyright 2018, Radiant Solutions, 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 org.venice.beachfront.bfapi.services;

import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.net.URI;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;

import org.apache.commons.codec.binary.Base64;
import org.apache.commons.io.IOUtils;
import org.joda.time.DateTime;
import org.joda.time.Duration;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.scheduling.annotation.Async;
import org.springframework.scheduling.annotation.EnableAsync;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;
import org.springframework.web.client.HttpClientErrorException;
import org.springframework.web.client.HttpServerErrorException;
import org.springframework.web.client.RestTemplate;
import org.venice.beachfront.bfapi.model.Algorithm;
import org.venice.beachfront.bfapi.model.Job;
import org.venice.beachfront.bfapi.model.Scene;
import org.venice.beachfront.bfapi.model.exception.UserException;
import org.venice.beachfront.bfapi.model.piazza.StatusMetadata;
import org.venice.beachfront.bfapi.services.JobService.JobStatusCallback;

import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ArrayNode;

import model.logger.Severity;
import util.PiazzaLogger;

@Service
@EnableAsync
public class PiazzaService {
    @Value("${piazza.server}")
    private String PIAZZA_URL;
    @Value("${PIAZZA_API_KEY}")
    private String PIAZZA_API_KEY;

    @Autowired
    private RestTemplate restTemplate;
    @Autowired
    private ObjectMapper objectMapper;
    @Autowired
    private PiazzaLogger piazzaLogger;
    @Autowired
    private SceneService sceneService;

    @Async
    public void execute(String serviceId, Algorithm algorithm, String userId, String jobId, Boolean computeMask,
            String jobName, CompletableFuture<Scene> sceneFuture, JobStatusCallback callback) {
        String piazzaJobUrl = String.format("%s/job", PIAZZA_URL);
        piazzaLogger.log(String.format(
                "Preparing to submit Execute Job request to Piazza at %s to Service ID %s by User %s.",
                piazzaJobUrl, serviceId, userId), Severity.INFORMATIONAL);

        // Ensure that the Scene has finished activating before proceeding with the Piazza execution.
        Scene scene = null;
        // capture when activation began
        DateTime activationStart = new DateTime();
        try {
            piazzaLogger.log(String.format("Waiting for Activation for Job %s", jobId), Severity.INFORMATIONAL);
            scene = sceneFuture.get();

            // calculate diff between now and when job started activation
            piazzaLogger.log(String.format(
                    "Job %s Scene has been activated for Scene ID %s, Scene platorm: %s, completed activation in %d seconds",
                    jobId, Scene.parseExternalId(scene.getSceneId()), Scene.parsePlatform(scene.getSceneId()),
                    new Duration(activationStart, new DateTime()).getStandardSeconds()), Severity.INFORMATIONAL);
        } catch (InterruptedException | ExecutionException e) {
            piazzaLogger.log(String.format("Getting Active Scene failed for Job %s : %s", jobId, e.getMessage()),
                    Severity.ERROR);
            callback.updateStatus(jobId, Job.STATUS_ERROR, "Activation timeout");

            // calculate diff between now and when job started activation
            piazzaLogger.log(
                    String.format("Job %s failed activation in %d seconds.", jobId,
                            new Duration(activationStart, new DateTime()).getStandardSeconds()),
                    Severity.INFORMATIONAL);

            return;
        }

        // Generate the Algorithm CLI
        // Formulate the URLs for the Scene
        List<String> fileNames;
        List<String> fileUrls;
        try {
            fileNames = sceneService.getSceneInputFileNames(scene);
            fileUrls = sceneService.getSceneInputURLs(scene);
        } catch (Exception exception) {
            exception.printStackTrace();
            piazzaLogger.log(
                    String.format("Could not get Asset Information for Job %s: %s", jobId, exception.getMessage()),
                    Severity.ERROR);
            callback.updateStatus(jobId, Job.STATUS_ERROR, "Scene metadata error");
            return;
        }

        // Prepare Job Request
        String algorithmCli = getAlgorithmCli(algorithm.getName(), fileNames,
                Scene.parsePlatform(scene.getSceneId()), computeMask);
        piazzaLogger.log(String.format("Generated CLI Command for Job %s (Scene %s) for User %s : %s", jobName,
                scene.getSceneId(), userId, algorithmCli), Severity.INFORMATIONAL);

        // Generate the Headers for Execution, including the API Key
        HttpHeaders headers = createPiazzaHeaders(PIAZZA_API_KEY);
        // Structure the Job Request
        String requestJson = null;
        try {
            // Add quotations to each element in the files lists, to ensure that JSON has the quotes after the
            // string-replace.
            List<String> quotedFileNames = new ArrayList<>();
            List<String> quotedFileUrls = new ArrayList<>();
            for (String fileName : fileNames) {
                quotedFileNames.add(String.format("\\\"%s\\\"", fileName));
            }
            for (String fileUrl : fileUrls) {
                quotedFileUrls.add(String.format("\\\"%s\\\"", fileUrl));
            }
            // Replace all user values into the execute request JSON template
            requestJson = String.format(loadJobRequestJson(), jobId, serviceId, algorithmCli,
                    String.join(", ", quotedFileUrls), String.join(", ", quotedFileNames), userId);
        } catch (Exception exception) {
            exception.printStackTrace();
            piazzaLogger.log(String.format("Could not load local resource file for Job Request for Job %s", jobId),
                    Severity.ERROR);
            callback.updateStatus(jobId, Job.STATUS_ERROR, "Error submitting job");
            return;
        }
        HttpEntity<String> request = new HttpEntity<>(requestJson, headers);

        // Execute the Request
        try {
            restTemplate.exchange(URI.create(piazzaJobUrl), HttpMethod.POST, request, String.class);
        } catch (HttpClientErrorException | HttpServerErrorException exception) {
            piazzaLogger.log(String.format(
                    "Piazza Job Request by User %s has failed with Code %s and Error %s. The body of the request was: %s",
                    userId, exception.getStatusText(), exception.getResponseBodyAsString(), requestJson),
                    Severity.ERROR);
            callback.updateStatus(jobId, Job.STATUS_ERROR, "Error submiting job");
            return;
        }

        // Update the Status of the Job as Submitted
        callback.updateStatus(jobId, Job.STATUS_SUBMITTED, null);
        // Log the Successful execution
        piazzaLogger.log(
                String.format("Received successful response from Piazza for Job %s by User %s.", jobId, userId),
                Severity.INFORMATIONAL);
    }

    /**
     * Gets the status of the Piazza Job with the specified Job ID
     * 
     * @param jobId
     *            The Job ID
     * @return The status of the Job, as returned by Piazza
     */
    public StatusMetadata getJobStatus(String jobId) throws UserException {
        String piazzaJobUrl = String.format("%s/job/%s", PIAZZA_URL, jobId);
        piazzaLogger.log(String.format("Checking Piazza Job Status for Job %s", jobId), Severity.INFORMATIONAL);
        HttpHeaders headers = createPiazzaHeaders(PIAZZA_API_KEY);
        HttpEntity<String> request = new HttpEntity<>(headers);

        // Execute the Request
        ResponseEntity<String> response = null;
        try {
            response = restTemplate.exchange(URI.create(piazzaJobUrl), HttpMethod.GET, request, String.class);
        } catch (HttpClientErrorException | HttpServerErrorException exception) {
            HttpStatus recommendedErrorStatus = exception.getStatusCode();
            if (recommendedErrorStatus.equals(HttpStatus.UNAUTHORIZED)) {
                recommendedErrorStatus = HttpStatus.BAD_REQUEST; // 401 Unauthorized logs out the client, and we don't
                // want that
            }

            String message = String.format("There was an error fetching Job Status from Piazza. (%d) id=%s",
                    exception.getStatusCode().value(), jobId);

            throw new UserException(message, exception.getMessage(), recommendedErrorStatus);
        }

        // Parse out the Status from the Response
        try {
            JsonNode responseJson = objectMapper.readTree(response.getBody());
            StatusMetadata status = new StatusMetadata(responseJson.get("data").get("status").asText());
            // Parse additional information depending on status
            if (status.isStatusSuccess()) {
                // If the status is complete, attach the Data ID of the shoreline detection
                status.setDataId(responseJson.get("data").get("result").get("dataId").asText());
            } else if (status.isStatusError()) {
                // If the status is errored, then attach the error information
                JsonNode messageNode = responseJson.get("data").get("message");
                if (messageNode != null) {
                    status.setErrorMessage(messageNode.asText());
                } else {
                    status.setErrorMessage(
                            "The Job contained an Error status but the cause was unable to be parsed from the response object.");
                }
                // If there is a detailed error message available in the Result field Data ID, fetch that ID.
                JsonNode resultNode = responseJson.get("data").get("result");
                if (resultNode != null && resultNode.get("dataId") != null) {
                    status.setDataId(resultNode.get("dataId").asText());
                }
            }
            return status;
        } catch (IOException | NullPointerException exception) {
            String error = String
                    .format("There was an error parsing the Piazza response when Requesting Job %s Status.", jobId);
            piazzaLogger.log(error, Severity.ERROR);
            throw new UserException(error, exception.getMessage(), HttpStatus.INTERNAL_SERVER_ERROR);
        }
    }

    /**
     * Returns the list of all algorithm services that have been registed with the Beachfront Piazza API Key
     * 
     * @return List of algorithms available for use in Beachfront
     */
    public List<Algorithm> getRegisteredAlgorithms() throws UserException {
        String piazzaServicesUrl = String.format("%s/service/me", PIAZZA_URL);
        piazzaLogger.log("Checking Piazza Registered Algorithms.", Severity.INFORMATIONAL);
        HttpHeaders headers = createPiazzaHeaders(PIAZZA_API_KEY);
        HttpEntity<String> request = new HttpEntity<>(headers);

        // Execute the Request
        ResponseEntity<String> response = null;
        try {
            response = restTemplate.exchange(URI.create(piazzaServicesUrl), HttpMethod.GET, request, String.class);
        } catch (HttpClientErrorException | HttpServerErrorException exception) {
            piazzaLogger.log(String.format("Error fetching Algorithms from Piazza with Code %s, Response was %s",
                    exception.getStatusText(), exception.getResponseBodyAsString()), Severity.ERROR);

            HttpStatus recommendedErrorStatus = exception.getStatusCode();
            if (recommendedErrorStatus.equals(HttpStatus.UNAUTHORIZED)) {
                recommendedErrorStatus = HttpStatus.BAD_REQUEST; // 401 Unauthorized logs out the client, and we don't
                // want that
            }

            String message = String.format("There was an error fetching Algorithm List from Piazza. (%d)",
                    exception.getStatusCode().value());
            throw new UserException(message, exception.getMessage(), recommendedErrorStatus);
        }

        // Ensure the response succeeded
        if (!response.getStatusCode().is2xxSuccessful()) {
            // Error occurred - report back to the user
            throw new UserException("Piazza returned a non-OK status when requesting registered Algorithm List.",
                    response.getStatusCode().toString(), response.getStatusCode());
        }

        // Parse out the Algorithms from the Response
        try {
            JsonNode responseJson = objectMapper.readTree(response.getBody());
            JsonNode algorithmJsonArray = responseJson.get("data");
            List<Algorithm> algorithms = new ArrayList<>();
            for (JsonNode algorithmJson : algorithmJsonArray) {
                // For each Registered Service, wrap it in the Algorithm Object and add to the list
                algorithms.add(getAlgorithmFromServiceNode(algorithmJson));
            }
            piazzaLogger.log(
                    String.format("Returning full Piazza algorithm list. Found %s Algorithms.", algorithms.size()),
                    Severity.INFORMATIONAL);
            return algorithms;
        } catch (IOException exception) {
            String error = "There was an error parsing the Piazza response when Requesting registered Algorithm List.";
            piazzaLogger.log(error, Severity.ERROR);
            throw new UserException(error, exception.getMessage(), HttpStatus.INTERNAL_SERVER_ERROR);
        }
    }

    /**
     * Gets the registered algorithm from Piazza based on the Service ID. This can return services that are not owned by
     * the currently configured Piazza API Key
     * 
     * @param serviceId
     *            Service ID to fetch
     * @return The Service
     */
    public Algorithm getRegisteredAlgorithm(String serviceId) throws UserException {
        String piazzaServiceUrl = String.format("%s/service/%s", PIAZZA_URL, serviceId);
        piazzaLogger.log(String.format("Checking Piazza Registered Algorithm %s.", serviceId),
                Severity.INFORMATIONAL);
        HttpHeaders headers = createPiazzaHeaders(PIAZZA_API_KEY);
        HttpEntity<String> request = new HttpEntity<>(headers);

        // Execute the Request
        ResponseEntity<String> response = null;
        try {
            response = restTemplate.exchange(URI.create(piazzaServiceUrl), HttpMethod.GET, request, String.class);
        } catch (HttpClientErrorException | HttpServerErrorException exception) {
            piazzaLogger.log(
                    String.format("Error fetching Algorithm %s from Piazza with Code %s, Response was %s",
                            serviceId, exception.getStatusText(), exception.getResponseBodyAsString()),
                    Severity.ERROR);

            HttpStatus recommendedErrorStatus = exception.getStatusCode();
            if (recommendedErrorStatus.equals(HttpStatus.UNAUTHORIZED)) {
                recommendedErrorStatus = HttpStatus.BAD_REQUEST; // 401 Unauthorized logs out the client, and we don't
                // want that
            }

            String message = String.format("There was an error fetching Algorithm from Piazza. (%d) id=%s",
                    exception.getStatusCode().value(), serviceId);
            throw new UserException(message, exception.getMessage(), recommendedErrorStatus);
        }

        // Ensure the response succeeded
        if (!response.getStatusCode().is2xxSuccessful()) {
            // Error occurred - report back to the user
            throw new UserException(String
                    .format("Piazza returned a non-OK status when requesting registered Algorithm %s.", serviceId),
                    response.getStatusCode().toString(), response.getStatusCode());
        }

        // Parse out the Algorithms from the Response
        try {
            JsonNode responseJson = objectMapper.readTree(response.getBody());
            JsonNode algorithmJson = responseJson.get("data");
            return getAlgorithmFromServiceNode(algorithmJson);
        } catch (IOException exception) {
            String error = String.format(
                    "There was an error parsing the Piazza response when Requesting registered Algorithm %s.",
                    serviceId);
            piazzaLogger.log(error, Severity.ERROR);
            throw new UserException(error, exception.getMessage(), HttpStatus.INTERNAL_SERVER_ERROR);
        }
    }

    /**
     * Calls the data/file endpoint to download data from Piazza for the specified Data ID.
     * <p>
     * Piazza Data IDs for a successful job are the raw GeoJSON of the shoreline detection vectors for a successful Job
     * execution.
     * <p>
     * Piazza Data IDs for an unsuccessful job will contain the detailed JSON information for an error message on an
     * algorithm execution. This contains the stack trace and other information from the algorithm itself that details
     * the errors.
     * 
     * @param dataId
     *            Data ID
     * @return The bytes of the ingested data
     */
    public byte[] downloadData(String dataId) throws UserException {
        String piazzaDataUrl = String.format("%s/file/%s", PIAZZA_URL, dataId);
        piazzaLogger.log(String.format("Requesting data %s bytes from Piazza at %s", dataId, piazzaDataUrl),
                Severity.INFORMATIONAL);
        HttpHeaders headers = createPiazzaHeaders(PIAZZA_API_KEY);
        HttpEntity<String> request = new HttpEntity<>(headers);

        // Execute the Request
        ResponseEntity<String> response = null;
        try {
            response = restTemplate.exchange(URI.create(piazzaDataUrl), HttpMethod.GET, request, String.class);
        } catch (HttpClientErrorException | HttpServerErrorException exception) {
            piazzaLogger.log(
                    String.format(
                            "Error downloading Data Bytes for Data %s from Piazza. Failed with Code %s and Body %s",
                            dataId, exception.getStatusText(), exception.getResponseBodyAsString()),
                    Severity.ERROR);

            HttpStatus recommendedErrorStatus = exception.getStatusCode();
            if (recommendedErrorStatus.equals(HttpStatus.UNAUTHORIZED)) {
                recommendedErrorStatus = HttpStatus.BAD_REQUEST; // 401 Unauthorized logs out the client, and we don't
                // want that
            }

            String message = String.format(
                    "There was an upstream error fetching data bytes from Piazza. (%d) id=%s",
                    exception.getStatusCode().value(), dataId);

            throw new UserException(message, exception.getMessage(), recommendedErrorStatus);
        }

        byte[] data = response.getBody().getBytes();
        piazzaLogger.log(String.format("Successfully retrieved Bytes for Data %s from Piazza. File size was %s",
                dataId, data.length), Severity.INFORMATIONAL);
        return data;
    }

    /**
     * Deletes the Data Item in Piazza that stores the GeoJSON vectors. This is only intended to be called after the
     * GeoJSON has been successfully retrieved from Piazza and stored in the Detections table, and thus no longer
     * needed.
     * <p>
     * This method helps keep the database clean from duplicated datasets.
     * 
     * @param metaDataId
     *            The Beachfront ID corresponding with the Piazza metadata job. This will be used to infer the ID of the
     *            GeoJSON data item.
     */
    public void deleteGeoJsonJobData(String metaDataId) throws UserException {
        String geoJsonDataId = getGeoJsonDataIdFromMetadata(metaDataId);
        deleteData(geoJsonDataId);
    }

    /**
     * Requests deletion of a single Data item from Piazza.
     * 
     * @param dataId
     *            The ID of the Data to delete
     */
    private void deleteData(String dataId) throws UserException {
        try {
            // Form request
            String deleteUrl = String.format("%s/data/%s", PIAZZA_URL, dataId);
            piazzaLogger.log(String.format("Requesting data %s be deleted from Piazza at %s", dataId, deleteUrl),
                    Severity.INFORMATIONAL);
            HttpHeaders headers = createPiazzaHeaders(PIAZZA_API_KEY);
            HttpEntity<String> request = new HttpEntity<>(headers);
            // Request the Deletion
            restTemplate.exchange(URI.create(deleteUrl), HttpMethod.DELETE, request, String.class);
            // Log success
            piazzaLogger.log(String.format("Data %s successfully deleted from Piazza", dataId),
                    Severity.INFORMATIONAL);
        } catch (HttpClientErrorException | HttpServerErrorException exception) {
            piazzaLogger.log(String.format("Error Deleting Data %s from Piazza. Failed with Code %s and Body %s",
                    dataId, exception.getStatusText(), exception.getResponseBodyAsString()), Severity.ERROR);
            throw new UserException("Upstream error deleting data", exception, exception.getStatusCode());
        } catch (Exception exception) {
            exception.printStackTrace();
            piazzaLogger.log(String.format("Unexpected Error Deleting Data %s from Piazza. %s", dataId,
                    exception.getMessage()), Severity.ERROR);
            throw new UserException("Unexpected error deleting data", exception, HttpStatus.INTERNAL_SERVER_ERROR);
        }
    }

    /**
     * Downloads the data for a successful Beachfront Detection Service Job's Metadata.
     * <p>
     * The Data will be textual data containing all of the relevent metadata for the Detection job. As part of the
     * metadata contained in this Text Data, will be the Identifier to the Piazza Data Item that will contain the
     * GeoJSON detection. This function will parse out that Data ID and fetch the detection GeoJSON from that Data ID.
     * 
     * @param metaDataId
     *            The Data ID of the Service Execution Job Metadata
     * @param jobId
     *            The Job ID. Used mostly for just reporting accurate logging upon failures.
     * @return The raw GeoJSON bytes of the shoreline detection.
     */
    public byte[] getJobResultBytesAsGeoJson(String metaDataId, String jobId) throws UserException {
        piazzaLogger.log(String.format(
                "Attempting to download GeoJSON data from Successful Job %s with Metadata Data Id %s.", jobId,
                metaDataId), Severity.INFORMATIONAL);
        String geoJsonDataId = getGeoJsonDataIdFromMetadata(metaDataId);
        // Use the GeoJSON Data ID to fetch the raw GeoJSON Bytes from Piazza
        piazzaLogger.log(
                String.format("Fetching GeoJSON for Job %s with Data %s from Piazza.", jobId, geoJsonDataId),
                Severity.INFORMATIONAL);
        return downloadData(geoJsonDataId);
    }

    /**
     * Gets the Data ID of the GeoJSON dataset from the Metadata ID
     * <p>
     * Piazza stores two Data IDs for each Beachfront Job. One data item stores the textual metadata of the job run,
     * which contains an ID pointer to the second data item - which contains the raw GeoJSON bytes.
     * <p>
     * 
     * @param metaDataId
     *            The Metadata ID
     * @return The GeoJSON Data ID
     */
    private String getGeoJsonDataIdFromMetadata(String metaDataId) throws UserException {
        byte[] metadata = downloadData(metaDataId);
        // Parse the real GeoJSON Data ID from the Metadata block
        try {
            JsonNode metadataJson = objectMapper.readTree(metadata);
            return metadataJson.get("OutFiles").get("shoreline.geojson").asText();
        } catch (IOException exception) {
            String error = String.format(
                    "There was an error parsing the Detection Metadata for Metadata Data Id %s. The raw content was: %s",
                    metaDataId, new String(metadata));
            piazzaLogger.log(error, Severity.ERROR);
            throw new UserException(error, exception, HttpStatus.INTERNAL_SERVER_ERROR);
        }
    }

    /**
     * Attempts to parse the detailed error information from a Data ID from the result of a failed job in Piazza. This
     * Data ID contains the raw stack trace information and std output from the algorithm itself. It also contains
     * user-friendly error messages that describe the failure.
     * <p>
     * This method will return the simple user-facing error information from this Error content payload.
     * 
     * @param dataId
     *            The Data ID of the failed Piazza details
     * @return The JsonNode of the user-facing error information
     */
    public String getDataErrorInformation(String dataId) throws UserException {
        // Download the raw error details
        byte[] errorDetails = downloadData(dataId);
        try {
            // Read the escaped JSON error message from the content field.
            JsonNode content = objectMapper.readTree(errorDetails);
            // Parse the user-friendly Errors field from the Content
            JsonNode errorsNode = content.get("Errors");
            if (errorsNode != null && errorsNode.isArray()) {
                // The errors block gets more detailed in sequence, just pick the first one.
                String error = ((ArrayNode) errorsNode).get(0).asText();
                // Each individual value may have more detailed errors separated by a semicolon, just pick the first
                // one.
                if (error.contains(";")) {
                    error = error.substring(0, error.indexOf(";"));
                }
                // Sometimes the string is an array in brackets, if so, remove that first brace.
                if (error.startsWith("[")) {
                    error = error.substring(1, error.length());
                }
                if (error.endsWith("[")) {
                    error = error.substring(0, error.lastIndexOf("]"));
                }
                // Ensure there is an error. If not, just return something default.
                if (StringUtils.isEmpty(error)) {
                    error = "Unspecified error during processing";
                }
                return error;
            } else {
                throw new UserException(String
                        .format("Error information in Data %s could not be found in the error content.", dataId),
                        HttpStatus.INTERNAL_SERVER_ERROR);
            }
        } catch (Exception exception) {
            throw new UserException(
                    String.format("Could not read error information from Content node for Data %s: %s", dataId,
                            exception.getMessage()),
                    HttpStatus.BAD_REQUEST);
        }

    }

    /**
     * Returns all of the Statistics for the Beachfront Algorithm as reported by the Piazza Task-Managed service.
     * 
     * @return JSON block containing statistics. This contains, at least, the number of jobs in that algorithms queue.
     */
    public JsonNode getAlgorithmStatistics(String algorithmId) throws UserException {
        String piazzaTaskUrl = String.format("%s/service/%s/task/metadata", PIAZZA_URL, algorithmId);
        piazzaLogger.log(
                String.format("Fetching Algorithm Tasks Metadata for %s at URL %s", algorithmId, piazzaTaskUrl),
                Severity.INFORMATIONAL);
        HttpHeaders headers = createPiazzaHeaders(PIAZZA_API_KEY);
        HttpEntity<String> request = new HttpEntity<>(headers);

        // Execute the Request
        ResponseEntity<String> response = null;
        try {
            response = restTemplate.exchange(URI.create(piazzaTaskUrl), HttpMethod.GET, request, String.class);
        } catch (HttpClientErrorException | HttpServerErrorException exception) {
            HttpStatus recommendedErrorStatus = exception.getStatusCode();
            if (recommendedErrorStatus.equals(HttpStatus.UNAUTHORIZED)) {
                recommendedErrorStatus = HttpStatus.BAD_REQUEST; // 401 Unauthorized logs out the client, and we don't
                // want that
            }

            String message = String.format("There was an error fetching Service Metadata from Piazza (%d) id=%s",
                    exception.getStatusCode().value(), algorithmId);
            throw new UserException(message, exception.getMessage(), recommendedErrorStatus);
        }

        try {
            return objectMapper.readTree(response.getBody());
        } catch (IOException exception) {
            String error = String.format("There was an error parsing the Service Metadata for service %s.",
                    algorithmId);
            piazzaLogger.log(error, Severity.ERROR);
            throw new UserException(error, exception, HttpStatus.INTERNAL_SERVER_ERROR);
        }
    }

    /**
     * Wraps up a Piazza Service JSON Node (from a /service response) in an Algorithm Object
     * 
     * @param serviceNode
     *            The Service Data Node
     * @return The Algorithm
     */
    private Algorithm getAlgorithmFromServiceNode(JsonNode algorithmJson) {
        JsonNode metadata = algorithmJson.get("resourceMetadata");
        JsonNode addedMetadata = metadata.get("metadata");
        return new Algorithm(metadata.get("description").asText(), addedMetadata.get("Interface").asText(),
                addedMetadata.get("ImgReq - cloudCover").asInt(), metadata.get("name").asText(),
                algorithmJson.get("serviceId").asText(), metadata.get("version").asText());
    }

    /**
     * Creates Basic Auth headers with the Piazza API Key
     * 
     * @param piazzaApiKey
     *            The Piazza API Key
     * @return Basic Auth Headers for Piazza
     */
    private HttpHeaders createPiazzaHeaders(String piazzaApiKey) {
        String plainCreds = piazzaApiKey + ":";
        byte[] plainCredsBytes = plainCreds.getBytes();
        byte[] base64CredsBytes = Base64.encodeBase64(plainCredsBytes);
        String base64Creds = new String(base64CredsBytes);
        HttpHeaders headers = new HttpHeaders();
        headers.add("Authorization", "Basic " + base64Creds);
        headers.setContentType(MediaType.APPLICATION_JSON);
        return headers;
    }

    /**
     * Gets the JSON for a Piazza job request, with string format parameters that must be filled in
     * 
     * @return JSON job request template
     */
    private String loadJobRequestJson() throws Exception {
        // Create the JSON Payload for the Layer request to GeoServer
        ClassLoader classLoader = getClass().getClassLoader();
        InputStream jsonStream = null;
        String jsonString = null;
        try {
            jsonStream = classLoader
                    .getResourceAsStream(String.format("%s%s%s", "piazza", File.separator, "execute.json"));
            jsonString = IOUtils.toString(jsonStream, "UTF-8");
        } finally {
            try {
                if (jsonStream != null) {
                    jsonStream.close();
                }
            } catch (Exception exception) {
                exception.printStackTrace();
            }
        }
        return jsonString;
    }

    /**
     * Gets the algorithm CLI command that will be passed to the algorithm through Piazza
     * 
     * @param algorithmName
     *            The name of the algorithm
     * @param fileNames
     *            The array of file names
     * @param scenePlatform
     *            The scene platform (source)
     * @param computeMask
     *            True if compute mask is to be applied, false if not
     * @return The full command line string that can be executed by the Service Executor
     */
    private String getAlgorithmCli(String algorithmId, List<String> fileUrls, String scenePlatform,
            boolean computeMask) {
        List<String> imageFlags = new ArrayList<>();
        // Generate the images string parameters
        for (String fileUrl : fileUrls) {
            imageFlags.add(String.format("-i %s", fileUrl));
        }
        // Generate Bands based on the platform
        String bandsFlag = null;
        switch (scenePlatform) {
        case Scene.PLATFORM_PLANET_LANDSAT:
        case Scene.PLATFORM_LOCALINDEX_LANDSAT:
        case Scene.PLATFORM_PLANET_SENTINEL_FROM_S3:
            bandsFlag = "--bands 1 1";
            break;
        case Scene.PLATFORM_PLANET_PLANETSCOPE:
            bandsFlag = "--bands 2 4";
            break;
        case Scene.PLATFORM_PLANET_RAPIDEYE:
            bandsFlag = "--bands 2 5";
            break;
        }
        // Piece together the CLI
        StringBuilder command = new StringBuilder();
        command.append(String.join(" ", imageFlags));
        if (bandsFlag != null) {
            command.append(" ");
            command.append(bandsFlag);
        }
        command.append(" --basename shoreline --smooth 1.0");
        if (computeMask) {
            command.append(" --coastmask");
        }
        return command.toString();
    }
}