package com.uploadcare.api;
import com.uploadcare.data.DataWrapper;
import com.uploadcare.data.PageData;
import com.uploadcare.exceptions.UploadcareApiException;
import com.uploadcare.exceptions.UploadcareAuthenticationException;
import com.uploadcare.exceptions.UploadcareInvalidRequestException;
import com.uploadcare.exceptions.UploadcareNetworkException;
import com.uploadcare.urls.UrlParameter;
import org.apache.commons.codec.binary.Hex;
import org.apache.commons.codec.digest.DigestUtils;
import org.apache.http.HttpEntity;
import org.apache.http.HttpResponse;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.methods.HttpUriRequest;
import org.apache.http.client.utils.URIBuilder;
import org.apache.http.util.EntityUtils;
import java.io.IOException;
import java.io.InputStream;
import java.net.URI;
import java.security.GeneralSecurityException;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.text.SimpleDateFormat;
import java.util.Calendar;
import java.util.Date;
import java.util.GregorianCalendar;
import java.util.Iterator;
import java.util.List;
import java.util.TimeZone;
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import static com.uploadcare.urls.UrlUtils.trustedBuild;
/**
* A helper class for doing API calls to the Uploadcare API. Supports API version 0.4.
*
* TODO Support of throttled requests needs to be added
*/
public class RequestHelper {
private final Client client;
public static final String LIBRARY_VERSION = "3.1";
public static final String DATE_FORMAT = "EEE, dd MMM yyyy HH:mm:ss Z";
public static final String DATE_FORMAT_ISO_8601 = "yyyy-MM-dd'T'HH:mm:ss.SSSZ";
public static final TimeZone UTC = TimeZone.getTimeZone("UTC");
private static final String EMPTY_MD5 = DigestUtils.md5Hex("");
private static final String JSON_CONTENT_TYPE = "application/json";
RequestHelper(Client client) {
this.client = client;
}
public static String rfc2822(Date date) {
SimpleDateFormat dateFormat = new SimpleDateFormat(RequestHelper.DATE_FORMAT);
dateFormat.setTimeZone(UTC);
return dateFormat.format(date);
}
public static String iso8601(Date date) {
SimpleDateFormat dateFormat = new SimpleDateFormat(RequestHelper.DATE_FORMAT_ISO_8601);
dateFormat.setTimeZone(UTC);
return dateFormat.format(date);
}
public String makeSignature(HttpUriRequest request, String date)
throws NoSuchAlgorithmException, InvalidKeyException {
StringBuilder sb = new StringBuilder();
sb.append(request.getMethod())
.append("\n").append(EMPTY_MD5)
.append("\n").append(JSON_CONTENT_TYPE)
.append("\n").append(date)
.append("\n").append(request.getURI().getPath());
byte[] privateKeyBytes = client.getPrivateKey().getBytes();
SecretKeySpec signingKey = new SecretKeySpec(privateKeyBytes, "HmacSHA1");
Mac mac = Mac.getInstance("HmacSHA1");
mac.init(signingKey);
byte[] hmacBytes = mac.doFinal(sb.toString().getBytes());
return Hex.encodeHexString(hmacBytes);
}
public void setApiHeaders(HttpUriRequest request) {
Calendar calendar = new GregorianCalendar(UTC);
String formattedDate = rfc2822(calendar.getTime());
request.setHeader("Accept", "application/vnd.uploadcare-v0.4+json");
request.setHeader("Date", formattedDate);
request.setHeader("User-Agent",
String.format("javauploadcare/%s/%s", LIBRARY_VERSION, client.getPublicKey()));
String authorization;
if (client.isSimpleAuth()) {
authorization = "Uploadcare.Simple " + client.getPublicKey() + ":" + client
.getPrivateKey();
} else {
try {
String signature = makeSignature(request, formattedDate);
authorization = "Uploadcare " + client.getPublicKey() + ":" + signature;
} catch (GeneralSecurityException e) {
throw new UploadcareApiException("Error when signing the request", e);
}
}
request.setHeader("Authorization", authorization);
}
public <T> T executeQuery(HttpUriRequest request, boolean apiHeaders, Class<T> dataClass) {
if (apiHeaders) {
setApiHeaders(request);
}
try {
CloseableHttpResponse response = client.getHttpClient().execute(request);
checkResponseStatus(response);
try {
HttpEntity entity = response.getEntity();
String data = EntityUtils.toString(entity);
return client.getObjectMapper().readValue(data, dataClass);
} finally {
response.close();
}
} catch (IOException e) {
throw new UploadcareNetworkException(e);
}
}
public static void setQueryParameters(URIBuilder builder, List<UrlParameter> parameters) {
for (UrlParameter parameter : parameters) {
builder.setParameter(parameter.getParam(), parameter.getValue());
}
}
public <T, U> Iterable<T> executePaginatedQuery(
final URI url,
final List<UrlParameter> urlParameters,
final boolean apiHeaders,
final Class<? extends PageData<U>> dataClass,
final DataWrapper<T, U> dataWrapper) {
return new Iterable<T>() {
public Iterator<T> iterator() {
return new Iterator<T>() {
private URI next = null;
private boolean more;
private Iterator<U> pageIterator;
{
getNext();
}
private void getNext() {
URI pageUrl;
if (next == null) {
URIBuilder builder = new URIBuilder(url);
setQueryParameters(builder, urlParameters);
pageUrl = trustedBuild(builder);
} else {
pageUrl = next;
}
PageData<U> pageData = executeQuery(new HttpGet(pageUrl), apiHeaders,
dataClass);
more = pageData.hasMore();
next = pageData.getNext();
pageIterator = pageData.getResults().iterator();
}
public boolean hasNext() {
if (pageIterator.hasNext()) {
return true;
} else if (more) {
getNext();
return true;
} else {
return false;
}
}
public T next() {
return dataWrapper.wrap(pageIterator.next());
}
public void remove() {
throw new UnsupportedOperationException();
}
};
}
};
}
/**
* Executes the request et the Uploadcare API and return the HTTP Response object.
*
* The existence of this method(and it's return type) enables the end user to extend the
* functionality of the
* Uploadcare API client by creating a subclass of {@link com.uploadcare.api.Client}.
*
* @param request request to be sent to the API
* @param apiHeaders TRUE if the default API headers should be set
* @return HTTP Response object
*/
public HttpResponse executeCommand(HttpUriRequest request, boolean apiHeaders) {
if (apiHeaders) {
setApiHeaders(request);
}
try {
CloseableHttpResponse response = client.getHttpClient().execute(request);
try {
checkResponseStatus(response);
return response;
} finally {
response.close();
}
} catch (IOException e) {
throw new UploadcareNetworkException(e);
}
}
/**
* Verifies that the response status codes are within acceptable boundaries and throws
* corresponding exceptions
* otherwise.
*
* @param response The response object to be checked
*/
private void checkResponseStatus(HttpResponse response) throws IOException {
int statusCode = response.getStatusLine().getStatusCode();
if (statusCode >= 200 && statusCode < 300) {
return;
} else if (statusCode == 401 || statusCode == 403) {
throw new UploadcareAuthenticationException(
streamToString(response.getEntity().getContent()));
} else if (statusCode == 400 || statusCode == 404) {
throw new UploadcareInvalidRequestException(
streamToString(response.getEntity().getContent()));
} else {
throw new UploadcareApiException(
"Unknown exception during an API call, response:" + streamToString(
response.getEntity().getContent()));
}
}
/**
* Convert an InputStream into a String object. Method taken from http://stackoverflow.com/a/5445161/521535
*
* @param is The stream to be converted
* @return The resulting String
*/
private static String streamToString(InputStream is) {
java.util.Scanner s = new java.util.Scanner(is, "UTF-8").useDelimiter("\\A");
return s.hasNext() ? s.next() : "";
}
}