package uk.ac.ox.zoo.seeg.abraid.mp.modelwrapper.model; import net.lingala.zip4j.core.ZipFile; import net.lingala.zip4j.exception.ZipException; import net.lingala.zip4j.model.ZipParameters; import net.lingala.zip4j.util.Zip4jConstants; import org.apache.commons.io.FileUtils; import org.apache.log4j.Logger; import org.springframework.util.StringUtils; import uk.ac.ox.zoo.seeg.abraid.mp.common.domain.ModelRunStatus; import uk.ac.ox.zoo.seeg.abraid.mp.common.dto.json.AbraidJsonObjectMapper; import uk.ac.ox.zoo.seeg.abraid.mp.common.dto.json.JsonModelOutputsMetadata; import uk.ac.ox.zoo.seeg.abraid.mp.common.web.ModelOutputConstants; import uk.ac.ox.zoo.seeg.abraid.mp.common.web.WebServiceClientException; import java.io.File; import java.io.IOException; import java.nio.file.Path; import java.util.ArrayList; /** * Provides a mechanism for reporting model completion or failure (and results) to the model output handler. * Copyright (c) 2014 University of Oxford */ public class ModelStatusReporterImpl implements ModelStatusReporter { private static Logger logger = Logger.getLogger(ModelStatusReporterImpl.class); // not final for test mocking private static final String LOG_HANDLER_REQUESTING = "Sending model outputs to model output handler " + "(zip file size %s)..."; private static final String LOG_HANDLER_REQUEST_ERROR = "Error sending model outputs for handling: %s"; private static final String LOG_HANDLER_RESPONSE_ERROR = "Error received from model output handler: %s"; private static final String LOG_HANDLER_SUCCESSFUL = "Successfully sent model outputs to model output handler."; private static final String LOG_COULD_NOT_DELETE_TEMP_FILE = "Could not delete temporary file \"%s\""; private static final String LOG_COULD_NOT_DELETE_WORKSPACE_DIR = "Could not delete workspace directory \"%s\""; private static final String LOG_NOT_DELETED_WORKSPACE_DIR = "Did not delete workspace directory \"%s\", as the model run did not complete successfully"; private final String runName; private final Path workingDirectoryPath; private final boolean deleteWorkspace; private final ModelOutputHandlerWebService modelOutputHandlerWebService; private final AbraidJsonObjectMapper objectMapper; public ModelStatusReporterImpl(String runName, Path workingDirectory, boolean deleteWorkspace, ModelOutputHandlerWebService modelOutputHandlerWebService, AbraidJsonObjectMapper objectMapper) { this.runName = runName; this.workingDirectoryPath = workingDirectory; this.deleteWorkspace = deleteWorkspace; this.modelOutputHandlerWebService = modelOutputHandlerWebService; this.objectMapper = objectMapper; } /** * Report model completion or failure (and results) to the model output handler. * @param status The model status. * @param outputText The output text from the model. * @param errorText The error text (and/or exception text) from the model. */ @Override public void report(ModelRunStatus status, String outputText, String errorText) { File zipFile = null; try { doesWorkingDirectoryExist(); createMetadataAndSaveToFile(status, outputText, errorText); zipFile = createZipFile(pickOutputFiles(status)); sendOutputsToModelOutputHandler(zipFile); if (deleteWorkspace) { deleteWorkingDirectory(status); } } catch (Exception e) { logger.fatal(String.format(LOG_HANDLER_REQUEST_ERROR, e.getMessage()), e); } finally { if (zipFile != null) { deleteZipFile(zipFile); } } } private static String[] pickOutputFiles(ModelRunStatus status) { if (status == ModelRunStatus.COMPLETED) { return new String[] { ModelOutputConstants.METADATA_JSON_FILENAME, ModelOutputConstants.MEAN_PREDICTION_RASTER_FILENAME, ModelOutputConstants.PREDICTION_UNCERTAINTY_RASTER_FILENAME, ModelOutputConstants.VALIDATION_STATISTICS_FILENAME, ModelOutputConstants.RELATIVE_INFLUENCE_FILENAME, ModelOutputConstants.EFFECT_CURVES_FILENAME, ModelOutputConstants.EXTENT_INPUT_RASTER_FILENAME }; } else { return new String[] { ModelOutputConstants.METADATA_JSON_FILENAME }; } } private void doesWorkingDirectoryExist() { File workingDirectory = workingDirectoryPath.toFile(); if (!workingDirectory.exists()) { throw new IllegalArgumentException(String.format("working directory \"%s\" does not exist", workingDirectory.getAbsolutePath())); } } private void createMetadataAndSaveToFile(ModelRunStatus status, String outputText, String errorText) throws IOException { // Create metadata and serialize as JSON JsonModelOutputsMetadata metadata = new JsonModelOutputsMetadata(runName, status, outputText, errorText); String metadataJson = objectMapper.writeValueAsString(metadata); // Write metadata to a file File metadataFile = getFileInWorkingDirectory(ModelOutputConstants.METADATA_JSON_FILENAME); FileUtils.writeStringToFile(metadataFile, metadataJson); } private File createZipFile(String[] outputFilenames) throws IOException, ZipException { // We want a temporary filename but the file must not yet exist. So create a temporary file then delete it. File file = File.createTempFile("outputs", ".zip"); deleteZipFile(file); ZipFile zipFile = new ZipFile(file); ZipParameters zipParameters = new ZipParameters(); zipParameters.setCompressionLevel(Zip4jConstants.DEFLATE_LEVEL_NORMAL); // Add files to zip file ArrayList<File> filesToAdd = new ArrayList<>(); for (String outputFilename : outputFilenames) { File fileToAdd = getFileInWorkingDirectory(outputFilename); if (fileToAdd.exists()) { filesToAdd.add(fileToAdd); } else { throw new IllegalArgumentException("File does not exist: " + fileToAdd.getAbsolutePath()); } } zipFile.createZipFile(filesToAdd, zipParameters); return file; } private void sendOutputsToModelOutputHandler(File zipFile) throws IOException, WebServiceClientException { logger.info(String.format(LOG_HANDLER_REQUESTING, zipFile.length())); String responseText = modelOutputHandlerWebService.handleOutputs(zipFile); if (StringUtils.hasText(responseText)) { logger.error(String.format(LOG_HANDLER_RESPONSE_ERROR, responseText)); } else { logger.info(LOG_HANDLER_SUCCESSFUL); } } private void deleteZipFile(File zipFile) { if (!zipFile.delete()) { logger.error(String.format(LOG_COULD_NOT_DELETE_TEMP_FILE, zipFile.getAbsolutePath())); } } private void deleteWorkingDirectory(ModelRunStatus modelRunStatus) { if (!modelRunStatus.equals(ModelRunStatus.COMPLETED)) { logger.warn(String.format(LOG_NOT_DELETED_WORKSPACE_DIR, workingDirectoryPath.toAbsolutePath())); } else { try { FileUtils.deleteDirectory(workingDirectoryPath.toFile()); } catch (IOException e) { logger.error(String.format(LOG_COULD_NOT_DELETE_WORKSPACE_DIR, workingDirectoryPath.toAbsolutePath())); } } } private File getFileInWorkingDirectory(String fileName) { return new File(workingDirectoryPath.toFile(), fileName); } }