/**
  * Stream data from specified payload into a file in our temp cache.
  *
  * @param object: The digital object to use
  * @param pid: The payload ID to extract
  * @return File: The cached File
  * @throws FileNotFoundException: If accessing the cache fails
  * @throws StorageException: If accessing the object in storage fails
  * @throws IOException: If the data copy fails
  */
 private File cacheFile(DigitalObject object, String pid)
     throws FileNotFoundException, StorageException, IOException {
   // Get our cache location
   File file = new File(outputDir, pid);
   FileOutputStream tempFileOut = new FileOutputStream(file);
   // Get payload from storage
   Payload payload = object.getPayload(pid);
   try {
     // Copy to cache
     IOUtils.copy(payload.open(), tempFileOut);
   } catch (IOException ex) {
     payload.close();
     throw ex;
   }
   // Close and return
   payload.close();
   tempFileOut.close();
   return file;
 }
  /**
   * Compile all available metadata and add to object.
   *
   * <p>Note that a False response indicates data has been written into the 'errors' map and should
   * be followed by an 'errorAndClose()' call.
   *
   * @return: boolean: False if any errors occurred, True otherwise
   */
  private boolean compileMetadata(DigitalObject object) {
    // Nothing to do
    if (info == null) {
      return true;
    }
    if (!info.isSupported()) {
      return true;
    }

    // Get base metadata of source
    JsonConfigHelper fullMetadata = null;
    try {
      fullMetadata = new JsonConfigHelper(info.toString());
    } catch (IOException ex) {
      addError("metadata", "Error parsing metadata output", ex);
      return false;
    }

    // Add individual conversion(s) metadata
    if (!metadata.isEmpty()) {
      fullMetadata.setJsonMap("outputs", metadata);
    }

    // Write the file to disk
    File metaFile;
    try {
      // log.debug("\nMetadata:\n{}", fullMetadata.toString());
      metaFile = writeMetadata(fullMetadata.toString());
      if (metaFile == null) {
        addError("metadata", "Unknown error extracting metadata");
        return false;
      }
    } catch (TransformerException ex) {
      addError("metadata", "Error writing metadata to disk", ex);
      return false;
    }

    // Store metadata
    try {
      Payload payload = createFfmpegPayload(object, metaFile);
      payload.setType(PayloadType.Enrichment);
      payload.close();
    } catch (Exception ex) {
      addError("metadata", "Error storing metadata payload", ex);
      return false;
    } finally {
      metaFile.delete();
    }

    // Everything should be fine if we got here
    return true;
  }
 /**
  * Read FFMPEG metadata from the object if it exists
  *
  * @param object: The object to extract data from
  */
 private void readMetadata(DigitalObject object) {
   Set<String> pids = object.getPayloadIdList();
   if (pids.contains(METADATA_PAYLOAD)) {
     try {
       Payload payload = object.getPayload(METADATA_PAYLOAD);
       JsonConfigHelper data = new JsonConfigHelper(payload.open());
       payload.close();
       oldMetadata = data.getJsonMap("outputs");
       // for (String k : oldMetadata.keySet()) {
       // log.debug("\n====\n{}\n===\n{}", k, oldMetadata.get(k));
       // }
     } catch (IOException ex) {
       log.error("Error parsing metadata JSON: ", ex);
     } catch (StorageException ex) {
       log.error("Error accessing metadata payload: ", ex);
     }
   }
 }
  /**
   * Transforming digital object method
   *
   * @params object: DigitalObject to be transformed
   * @return transformed DigitalObject after transformation
   * @throws TransformerException if the transformation fails
   */
  @Override
  public DigitalObject transform(DigitalObject object, String jsonConfig)
      throws TransformerException {
    if (testExecLevel() == null) {
      log.error("FFmpeg is either not installed, or not executing!");
      return object;
    }
    // Purge old data
    reset();
    oid = object.getId();
    outputDir = new File(outputRoot, oid);
    outputDir.mkdirs();

    try {
      itemConfig = new JsonConfigHelper(jsonConfig);
    } catch (IOException ex) {
      throw new TransformerException("Invalid configuration! '{}'", ex);
    }

    // Resolve multi-segment files first
    mergeRate = get(itemConfig, "merging/mpegFrameRate", "25");
    finalRate = get(itemConfig, "merging/finalFrameRate", "10");
    finalFormat = get(itemConfig, "merging/finalFormat", "avi");
    mergeSegments(object);

    // Find the format 'group' this file is in
    String sourceId = object.getSourceId();
    String ext = FilenameUtils.getExtension(sourceId);
    format = getFormat(ext);

    // Return now if this isn't a format we care about
    if (format == null) {
      return object;
    }
    // log.debug("Supported format found: '{}' => '{}'", ext, format);

    // Cache the file from storage
    File file;
    try {
      file = cacheFile(object, sourceId);
    } catch (IOException ex) {
      addError(sourceId, "Error writing temp file", ex);
      errorAndClose(object);
      return object;
    } catch (StorageException ex) {
      addError(sourceId, "Error accessing storage data", ex);
      errorAndClose(object);
      return object;
    }
    if (!file.exists()) {
      addError(sourceId, "Unknown error writing cache: does not exist");
      errorAndClose(object);
      return object;
    }

    // **************************************************************
    // From here on we know (assume) that we SHOULD be able to support
    // this object, so errors can't just throw exceptions. We should
    // only return under certain circumstances (ie. not just because
    // one rendition fails), and the object must get closed.
    // **************************************************************

    // Read any pre-existing rendition metadata from previous tranformations
    // ++++++++++++++++++++++++++++
    // TODO: This is useless until the last modified date can be retrieved
    // against the source file. Storage API does not currently support this,
    // it just returns a data stream.
    //
    // Once this feature exists the basic algorithm should be:
    // 1) Retrieve old metadata
    // 2) Loop through each rendition preparation as normal
    // 3) When the transcoding is ready to start, use the parameters to
    // query the database for the last time the exact transcoding was
    // and comparing against last modifed.
    // 4) If the transcoding is newer than the source, skip running FFmpeg
    // and just use the same metadata as last time.
    // ++++++++++++++++++++++++++++
    // readMetadata(object);

    // Check for a custom display type
    String display = get(itemConfig, "displayTypes/" + format);
    if (display != null) {
      try {
        Properties prop = object.getMetadata();
        prop.setProperty("displayType", display);
        prop.setProperty("previewType", display);
      } catch (StorageException ex) {
        addError("display", "Could not access object metadata", ex);
      }
    }

    // Gather metadata
    try {
      info = ffmpeg.getInfo(file);
    } catch (IOException ex) {
      addError("metadata", "Error accessing metadata", ex);
      errorAndClose(object);
      return object;
    }

    // Can we even process this file?
    if (!info.isSupported()) {
      closeObject(object);
      return object;
    }

    // What conversions are required for this format?
    List<JsonConfigHelper> conversions = getJsonList(itemConfig, "transcodings/" + format);
    for (JsonConfigHelper conversion : conversions) {
      String name = conversion.get("alias");
      // And what/how many renditions does it have?
      List<JsonConfigHelper> renditions = conversion.getJsonList("renditions");
      if (renditions == null || renditions.isEmpty()) {
        addError(
            "transcodings",
            "Invalid or missing transcoding data:" + " '/transcodings/" + format + "'");
      } else {
        // Config look valid, lets give it a try
        // log.debug("Starting renditions for '{}'", name);
        for (JsonConfigHelper render : renditions) {
          File converted = null;
          // Render the output
          try {
            converted = convert(file, render, info);
          } catch (Exception ex) {
            String outputFile = render.get("name");
            if (outputFile != null) {
              addError(jsonKey(outputFile), "Error converting file", ex);
            } else {
              // Couldn't read the config for a name
              addError("unknown", "Error converting file", ex);
            }
          }

          // Now store the output if valid
          if (converted != null) {
            try {
              Payload payload = createFfmpegPayload(object, converted);
              // TODO: Type checking needs more work
              // Indexing fails silently if you add two thumbnails
              // or two previews
              payload.setType(resolveType(render.get("type")));
              payload.close();
            } catch (Exception ex) {
              addError(jsonKey(converted.getName()), "Error storing output", ex);
            } finally {
              converted.delete();
            }
          }
        }
      }
    }

    // Write metadata to storage
    if (compileMetadata(object)) {
      // Close normally
      closeObject(object);
    } else {
      // Close with some errors
      errorAndClose(object);
    }
    // Cleanup
    if (file.exists()) {
      file.delete();
    }
    return object;
  }