Package de.bitzeche.video.transcoding.zencoder

Source Code of de.bitzeche.video.transcoding.zencoder.ZencoderClient

/**
* Copyright (C) 2013 Bitzeche GmbH <info@bitzeche.de>
*
* 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 de.bitzeche.video.transcoding.zencoder;

import java.io.StringWriter;
import java.net.SocketTimeoutException;
import java.util.HashMap;
import java.util.Map;

import javax.ws.rs.core.MediaType;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.transform.OutputKeys;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerException;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.stream.StreamResult;
import javax.xml.xpath.XPath;
import javax.xml.xpath.XPathConstants;
import javax.xml.xpath.XPathExpressionException;
import javax.xml.xpath.XPathFactory;

import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;

import com.sun.jersey.api.client.Client;
import com.sun.jersey.api.client.ClientResponse;
import com.sun.jersey.api.client.UniformInterfaceException;
import com.sun.jersey.api.client.WebResource;
import com.sun.jersey.client.apache.ApacheHttpClient;

import de.bitzeche.video.transcoding.zencoder.enums.ZencoderAPIVersion;
import de.bitzeche.video.transcoding.zencoder.enums.ZencoderNotificationJobState;
import de.bitzeche.video.transcoding.zencoder.job.ZencoderJob;
import de.bitzeche.video.transcoding.zencoder.job.ZencoderOutput;
import de.bitzeche.video.transcoding.zencoder.response.ZencoderErrorResponseException;

public class ZencoderClient implements IZencoderClient {

  private static final Logger LOGGER = LoggerFactory
      .getLogger(ZencoderClient.class);
  private Client httpClient;
  private final String zencoderAPIBaseUrl;
  private final String zencoderAPIKey;
  private final ZencoderAPIVersion zencoderAPIVersion;
  private XPath xPath;
 
  private final int MAX_CONNECTION_ATTEMPTS = 5;
  private int currentConnectionAttempt = 0;

  public ZencoderClient(String zencoderApiKey) {
    this(zencoderApiKey, ZencoderAPIVersion.API_V1);
  }

  public ZencoderClient(String zencoderApiKey, ZencoderAPIVersion apiVersion) {
    this.zencoderAPIKey = zencoderApiKey;
    if (ZencoderAPIVersion.API_DEV.equals(apiVersion)) {
      LOGGER.warn("!!! Using development version of zencoder API !!!");
    }

    this.zencoderAPIVersion = apiVersion;

    httpClient = ApacheHttpClient.create();
    httpClient.setFollowRedirects(true);
   
    // set a 20 second timeout on the client
    httpClient.setConnectTimeout(20000);
    httpClient.setReadTimeout(20000);
   
    xPath = XPathFactory.newInstance().newXPath();
    zencoderAPIBaseUrl = zencoderAPIVersion.getBaseUrl();
  }

  /*
   * Typical response: <?xml version="1.0" encoding="UTF-8"?> <api-response>
   * <job> <test type="boolean">true</test> <outputs type="array"> <output>
   * <url
   * >http://audio-bucket.jagtest.spotnote.s3.amazonaws.com/ApU001TestUserAx001
   * .m4a</url> <label>test_aac</label> <id type="integer">29345822</id>
   * </output> </outputs> <id type="integer">17941347</id> </job>
   * </api-response>
   */

  private Integer findIdFromOutputNode(Node output)
      throws XPathExpressionException {
    Double idDouble = (Double) xPath.evaluate("output/id", output,
        XPathConstants.NUMBER);
    return idDouble == null ? null : idDouble.intValue();
  }

  /**
   * Complete output IDs from response.
   *
   * @param job
   * @param response
   */
  private void completeJobInfo(ZencoderJob job, Document response) {
    try {
      NodeList outputs = (NodeList) xPath.evaluate(
          "/api-response/job/outputs", response,
          XPathConstants.NODESET);
      if (job.getOutputs().size() == 1) {
        Integer id = findIdFromOutputNode(outputs.item(0));
        if (id != null) {
          job.getOutputs().get(0).setId(id);
        }
      } else {
        // try via labels
        Map<String, Integer> ids = new HashMap<String, Integer>();
        int outputSize = outputs.getLength();
        for (int i = 0; i < outputSize; i++) {
          String label = (String) xPath.evaluate("output/label",
              outputs.item(i), XPathConstants.STRING);
          if (label != null && !label.isEmpty()) {
            int id = findIdFromOutputNode(outputs.item(i));
            ids.put(label, new Integer(id));
          }
        }
        for (ZencoderOutput zcOutput : job.getOutputs()) {
          Integer foundId = ids.get(zcOutput.getLabel());
          if (foundId != null) {
            zcOutput.setId(foundId);
          }
        }
      }

    } catch (XPathExpressionException e) {
      LOGGER.error("XPath threw Exception", e);
    }
  }
 
  private void resetConnectionCount() {
    // reset to 0 for use in tracking connections next time
    currentConnectionAttempt = 0;
  }
 
  private Document createDocumentForException(String message) {
    try {
      DocumentBuilderFactory documentBuilderFactory = DocumentBuilderFactory.newInstance();
      DocumentBuilder documentBuilder = documentBuilderFactory.newDocumentBuilder();
      Document errorDocument = documentBuilder.newDocument();
      Element root = errorDocument.createElement("error");
      errorDocument.appendChild(root);
      Node input = errorDocument.createElement("reason");
      input.setTextContent(message);
      root.appendChild(input);
      return errorDocument;
    } catch (ParserConfigurationException e) {
      LOGGER.error("Exception creating document for exception '" + message + "'", e);
      return null;
    }
  }

  @Override
  public Document createJob(ZencoderJob job)
      throws ZencoderErrorResponseException {
    if(currentConnectionAttempt > MAX_CONNECTION_ATTEMPTS) {
      resetConnectionCount();
      String message = "Reached maximum number of connection attempts for Zencoder, aborting creation of job";
      Document errorDocument = createDocumentForException(message);
      throw new ZencoderErrorResponseException(errorDocument);
    }
    Document data;
    try {
      data = job.createXML();
      if (data == null) {
        String message = "Got no XML from Job";
        LOGGER.error(message);
        resetConnectionCount();
        Document errorDocument = createDocumentForException(message);
        throw new ZencoderErrorResponseException(errorDocument);
      }
      Element apikey = data.createElement("api_key");
      apikey.setTextContent(zencoderAPIKey);
      data.getDocumentElement().appendChild(apikey);
      Document response = sendPostRequest(zencoderAPIBaseUrl
          + "jobs?format=xml", data);
      // a null response means the call did not get through
      // we should try again, since the job has not been started
      if(response == null) {
        currentConnectionAttempt++;
        // maybe delay this call by a few seconds?
        return createJob(job);
      }
      String id = (String) xPath.evaluate("/api-response/job/id",
          response, XPathConstants.STRING);
      if (StringUtils.isNotEmpty(id)) {
        job.setJobId(Integer.parseInt(id));
        resetConnectionCount();
        return response;
      }
      completeJobInfo(job, response);
      LOGGER.error("Error when sending request to Zencoder: ", response);
      resetConnectionCount();
      throw new ZencoderErrorResponseException(response);
    } catch (ParserConfigurationException e) {
      LOGGER.error("Parser threw Exception", e);
    } catch (XPathExpressionException e) {
      LOGGER.error("XPath threw Exception", e);
    }
    resetConnectionCount();
    return null;
  }

  public ZencoderNotificationJobState jobProgress(ZencoderJob job) {
    return jobProgress(job.getJobId());
  }

  public ZencoderNotificationJobState jobProgress(int id) {
    return getJobState(id);
  }

  public ZencoderNotificationJobState getJobState(ZencoderJob job) {
    return getJobState(job.getJobId());
  }

  public ZencoderNotificationJobState getJobState(int id) {
    Document response = getJobProgress(id);
    if (response == null) {
      return null;
    }
    String stateString = null;
    try {
      stateString = (String) xPath.evaluate("/api-response/state",
          response, XPathConstants.STRING);
      return ZencoderNotificationJobState.getJobState(stateString);
    } catch (IllegalArgumentException ex) {
      LOGGER.error("Unable to find state for string '{}'", stateString);
    } catch (XPathExpressionException e) {
      LOGGER.error("XPath threw Exception", e);
    }
    return null;
  }

  public Document getJobProgress(ZencoderJob job) {
    return getJobProgress(job.getJobId());
  }

  public Document getJobProgress(int id) {
    if(currentConnectionAttempt > MAX_CONNECTION_ATTEMPTS) {
      resetConnectionCount();
      LOGGER.error("Reached maximum number of attempts for getting job progress. Aborting and returning null");
      return null;
    }
    if (zencoderAPIVersion != ZencoderAPIVersion.API_V2) {
      LOGGER.warn("jobProgress is only available for API v2.  Returning null.");
      return null;
    }
    String url = zencoderAPIBaseUrl + "jobs/" + id
        + "/progress.xml?api_key=" + zencoderAPIKey;
    Document result = sendGetRequest(url);
    if(result == null) {
      currentConnectionAttempt++;
      // delay this call by a few seconds?
      return getJobProgress(id);
    }
    resetConnectionCount();
    return result;
  }

  public Document getJobDetails(ZencoderJob job) {
    return getJobDetails(job.getJobId());
  }

  public Document getJobDetails(int id) {
    if(currentConnectionAttempt > MAX_CONNECTION_ATTEMPTS) {
      resetConnectionCount();
      LOGGER.error("Reached maximum number of attempts for getting job details. Aborting and returning null");
      return null;
    }
    String url = zencoderAPIBaseUrl + "jobs/" + id + ".xml?api_key="
        + zencoderAPIKey;
    Document result = sendGetRequest(url);
    if(result == null) {
      currentConnectionAttempt++;
      // delay this call by a few seconds?
      return getJobDetails(id);
    }
    resetConnectionCount();
    return result;
  }

  public boolean resubmitJob(ZencoderJob job) {
    int id;
    if ((id = job.getJobId()) != 0) {
      return resubmitJob(id);
    }
    return false;
  }

  public boolean resubmitJob(int id) {
    if(currentConnectionAttempt > MAX_CONNECTION_ATTEMPTS) {
      resetConnectionCount();
      LOGGER.error("Reached maximum number of attempts to resubmit job, aborting");
      return false;
    }
    String url = zencoderAPIBaseUrl + "jobs/" + id + "/resubmit?api_key="
        + zencoderAPIKey;
    ClientResponse response = sendPutRequest(url);
    if(response == null) {
      currentConnectionAttempt++;
      return resubmitJob(id);
    }
    int responseStatus = response.getStatus();
    resetConnectionCount();
    if (responseStatus == 200 || responseStatus == 204) {
      return true;
    } else if (responseStatus == 409) {
      LOGGER.debug("Already finished job {}", id);
      return true;
    }
    return false;
  }

  public boolean cancelJob(ZencoderJob job) {
    int id;
    if ((id = job.getJobId()) != 0) {
      return cancelJob(id);
    }
    return false;
  }

  public boolean cancelJob(int id) {
    if(currentConnectionAttempt > MAX_CONNECTION_ATTEMPTS) {
      resetConnectionCount();
      LOGGER.error("Reached maximum number of attempts to cancel job, aborting");
      return false;
    }
    String url = zencoderAPIBaseUrl + "jobs/" + id
        + "/cancel.json?api_key=" + zencoderAPIKey;
    ClientResponse res = sendPutRequest(url);
    if(res == null) {
      currentConnectionAttempt++;
      return cancelJob(id);
    }
    int responseStatus = res.getStatus();
    resetConnectionCount();
    if (responseStatus == 200 || responseStatus == 204) {
      return true;
    } else if (responseStatus == 409) {
      LOGGER.debug("Already finished job {}", id);
      return true;
    }
    return false;
  }

  public boolean deleteJob(ZencoderJob job) {
    int id;
    if ((id = job.getJobId()) != 0) {
      return deleteJob(id);
    }
    return false;
  }

  @Deprecated
  public boolean deleteJob(int id) {
    LOGGER.warn("Deleting jobs is deprecated. Use cancel instead.");
    return cancelJob(id);

    // String url = zencoderAPIBaseUrl + "jobs/" + id + "?api_key="
    // + zencoderAPIKey;
    // LOGGER.debug("calling to delete job: {}", url);
    // WebResource webResource = httpClient.resource(url);
    // ClientResponse response = webResource.delete(ClientResponse.class);
    // int responseStatus = response.getStatus();
    // return (responseStatus == 200);
  }

  protected Document sendGetRequest(String url) {
    LOGGER.debug("calling: {}", url);
    try {
      WebResource webResource = httpClient.resource(url);
      Document response = webResource.get(Document.class);

      logXmlDocumentToDebug("Got response", response);
      return response;
    } catch (Exception e) {
      if(e instanceof SocketTimeoutException) {
        LOGGER.warn("Connection to Zencoder timed out");
      } else {
        LOGGER.warn(e.getMessage());
      }
    }
    return null;
  }

  protected ClientResponse sendPutRequest(String url) {
    LOGGER.debug("calling: {}", url);
    try {
      WebResource webResource = httpClient.resource(url);
      ClientResponse response = webResource.put(ClientResponse.class);

      LOGGER.debug("Got response: {}", response);
      return response;
    } catch (Exception e) {
      if(e instanceof SocketTimeoutException) {
        LOGGER.warn("Connection to Zencoder timed out");
      } else {
        LOGGER.warn(e.getMessage());
      }
    }
    return null;

  }

  protected Document sendPostRequest(String url, Document xml) {
    logXmlDocumentToDebug("submitting", xml);
    try {
      WebResource webResource = httpClient.resource(url);
      Document response = webResource.accept(MediaType.APPLICATION_XML)
          .header("Content-Type", "application/xml")
          .post(Document.class, xml);
      logXmlDocumentToDebug("Got response", response);
      return response;
    } catch (UniformInterfaceException e) {
      ClientResponse resp = e.getResponse();
      Document errorXml = resp.getEntity(Document.class);
      String errormessage = e.getMessage();
      try {
        errormessage = (String) xPath.evaluate(
            "/api-response/errors/error", errorXml,
            XPathConstants.STRING);
      } catch (XPathExpressionException e1) {
        // ignore
      }
      LOGGER.error("couldn't submit job: {}", errormessage);
      return errorXml;
    } catch (Exception e) {
      if(e instanceof SocketTimeoutException) {
        LOGGER.warn("Connection to Zencoder timed out");
      } else {
        LOGGER.warn(e.getMessage());
      }
    }
    return null;

  }

  private void logXmlDocumentToDebug(String message, Document response) {

    if (LOGGER.isDebugEnabled()) {
      try {
        LOGGER.debug(message + ": {}", XmltoString(response));
      } catch (TransformerException e2) {
      }
    }
  }

  protected static String XmltoString(Document document)
      throws TransformerException {
    StringWriter stringWriter = new StringWriter();
    StreamResult streamResult = new StreamResult(stringWriter);
    TransformerFactory transformerFactory = TransformerFactory
        .newInstance();
    Transformer transformer = transformerFactory.newTransformer();
    transformer.setOutputProperty(OutputKeys.INDENT, "yes");
    transformer.setOutputProperty(
        "{http://xml.apache.org/xslt}indent-amount", "2");
    transformer.setOutputProperty(OutputKeys.METHOD, "xml");
    transformer.transform(new DOMSource(document.getDocumentElement()),
        streamResult);
    return stringWriter.toString();
  }

  public void setHttpClient(Client httpClient) {
    this.httpClient = httpClient;
  }
}
TOP

Related Classes of de.bitzeche.video.transcoding.zencoder.ZencoderClient

TOP
Copyright © 2018 www.massapi.com. All rights reserved.
All source code are property of their respective owners. Java is a trademark of Sun Microsystems, Inc and owned by ORACLE Inc. Contact coftware#gmail.com.