Java tutorial
/** * 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; } }