/**
   * 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;
  }
 /**
  * Create ffmpeg error payload
  *
  * @param object : DigitalObject to store the payload
  * @return Payload the error payload
  * @throws FileNotFoundException if the file provided does not exist
  * @throws UnsupportedEncodingException for encoding errors in the message
  */
 public Payload createFfmpegErrorPayload(DigitalObject object)
     throws StorageException, FileNotFoundException, UnsupportedEncodingException {
   // Compile our error data
   JsonConfigHelper content = new JsonConfigHelper();
   content.setJsonMap("/", errors);
   log.debug("\nErrors:\n{}", content.toString());
   InputStream data = new ByteArrayInputStream(content.toString().getBytes("UTF-8"));
   // Write to the object
   Payload payload = StorageUtils.createOrUpdatePayload(object, ERROR_PAYLOAD, data);
   payload.setType(PayloadType.Error);
   payload.setContentType("application/json");
   payload.setLabel("FFMPEG conversion errors");
   return payload;
 }
  /**
   * Check the object for a multi-segment source and merge them. Such sources must come from a
   * harvester specifically designed to match this transformer. As such we can make certain
   * assumptions, and if they are not met we just fail silently with a log entry.
   *
   * @param object: The digital object to modify
   */
  private void mergeSegments(DigitalObject object) {
    try {
      // Retrieve (optional) segment information from metadata
      Properties props = object.getMetadata();
      String segs = props.getProperty("mediaSegments");
      if (segs == null) {
        return;
      }
      int segments = Integer.parseInt(segs);
      if (segments <= 1) {
        return;
      }

      // We need to do some merging, lets validate IDs first
      log.info("Found {} source segments! Merging...", segments);
      List<String> segmentIds = new ArrayList();
      Set<String> payloadIds = object.getPayloadIdList();
      // The first segment
      String sourceId = object.getSourceId();
      if (sourceId == null || !payloadIds.contains(sourceId)) {
        log.error("Cannot find source payload.");
        return;
      }
      segmentIds.add(sourceId);
      // Find the other segments
      for (int i = 1; i < segments; i++) {
        // We won't know the extension though
        String segmentId = "segment" + i + ".";
        for (String pid : payloadIds) {
          if (pid.startsWith(segmentId)) {
            segmentIds.add(pid);
          }
        }
      }

      // Did we find every segment?
      if (segmentIds.size() != segments) {
        log.error("Unable to find all segments in payload list.");
        return;
      }

      // Transcode all the files to neutral MPEGs first
      Map<String, File> files = new HashMap();
      for (String segment : segmentIds) {
        try {
          File file = basicMpeg(object, segment);
          if (file != null) {
            files.put(segment, file);
          }
        } catch (Exception ex) {
          log.error("Error transcoding segment to MPEG: ", ex);
          // Cleanup
          for (File f : files.values()) {
            if (f.exists()) {
              f.delete();
            }
          }
          return;
        }
      }

      // Did every transcoding succeed?
      if (files.size() != segments) {
        log.error("At least one segment transcoding failed.");
        // Cleanup
        for (File f : files.values()) {
          if (f.exists()) {
            f.delete();
          }
        }
        return;
      }

      // Now to try merging all the segments. In MPEG format
      // they can just be concatenated.
      try {
        // Create our output file
        String filename = "temp_" + MERGED_PAYLOAD + "mpg";
        File merged = new File(outputDir, filename);
        if (merged.exists()) {
          merged.delete();
        }
        FileOutputStream out = new FileOutputStream(merged);

        // Merge each segment in order
        for (String sId : segmentIds) {
          try {
            mergeSegment(out, files.get(sId));
          } catch (IOException ex) {
            log.error("Failed to stream to merged file: ", ex);
            out.close();
            // Cleanup
            for (File f : files.values()) {
              if (f.exists()) {
                f.delete();
              }
            }
            merged.delete();
            return;
          }
        }
        out.close();

        // Final step, run the output file through a transcoding to
        // write the correct metadata (eg. duration)
        filename = MERGED_PAYLOAD + finalFormat;
        File transcoded = new File(outputDir, filename);
        if (transcoded.exists()) {
          transcoded.delete();
        }

        // Render
        String stderr = mergeRender(merged, transcoded);
        log.debug("=====\n{}", stderr);
        if (transcoded.exists()) {
          // Now we need to 'fix' the object, add the new source
          FileInputStream fis = new FileInputStream(transcoded);
          String pid = transcoded.getName();
          Payload p = StorageUtils.createOrUpdatePayload(object, pid, fis);
          fis.close();
          p.setType(PayloadType.Source);
          object.setSourceId(pid);

          // Remove all the old segments
          for (String sId : segmentIds) {
            object.removePayload(sId);
          }
          props.remove("mediaSegments");
          object.close();

          // Cleanup segments
          for (File f : files.values()) {
            if (f.exists()) {
              f.delete();
            }
          }
          merged.delete();
          transcoded.delete();
        }
      } catch (IOException ex) {
        log.error("Error merging segments: ", ex);
      }

    } catch (StorageException ex) {
      log.error("Error accessing object metadata: ", 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;
  }