de.bitzeche.video.transcoding.zencoder.ZencoderClient.java Source code

Java tutorial

Introduction

Here is the source code for de.bitzeche.video.transcoding.zencoder.ZencoderClient.java

Source

/**
 * 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 Strign zencoderReadOnlyAPIKey;
    private final ZencoderAPIVersion zencoderAPIVersion;
    private XPath xPath;

    private final int MAX_CONNECTION_ATTEMPTS = 5;
    private int currentConnectionAttempt = 0;

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

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

    public ZencoderClient(String zencoderApiKey, ZencoderAPIVersion apiVersion) {
        this(zencoderApiKey, null, apiVersion);
    }

    public ZencoderClient(String zencoderApiKey, String zencoderReadOnlyApiKey, ZencoderAPIVersion apiVersion) {
        this.zencoderAPIKey = zencoderApiKey;
        this.zencoderReadOnlyAPIKey = zencoderReadOnlyApiKey;
        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 apiKey = zencoderAPIKey;
        if (zencoderReadOnlyAPIKey != null)
            apiKey = zencoderReadOnlyAPIKey;
        String url = zencoderAPIBaseUrl + "jobs/" + id + "/progress.xml?api_key=" + apiKey;
        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 apiKey = zencoderAPIKey;
        if (zencoderReadOnlyAPIKey != null)
            apiKey = zencoderReadOnlyAPIKey;
        String url = zencoderAPIBaseUrl + "jobs/" + id + ".xml?api_key=" + apiKey;
        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;
    }
}