// $codepro.audit.disable assignmentInCondition
package net.rdrei.android.scdl2.api;
import java.io.BufferedInputStream;
import java.io.BufferedReader;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.lang.reflect.Type;
import java.net.HttpURLConnection;
import java.net.URLConnection;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.Map.Entry;
import java.util.zip.GZIPInputStream;
import roboguice.util.Ln;
import com.github.kevinsawicki.http.HttpRequest;
import com.github.kevinsawicki.http.HttpRequest.SendCallback;
import com.google.gson.Gson;
import com.google.gson.reflect.TypeToken;
import com.google.inject.Inject;
import com.google.inject.Injector;
public abstract class AbstractSoundcloudApiQueryImpl<T extends SoundcloudEntity>
implements SoundcloudApiQuery<T> {
private static final String NEWLINE_FALLBACK = "\n";
private static final String GZIP = "gzip";
private static final String ACCEPT_HEADER_KEY = "Accept";
private static final String ACCEPT_HEADER_VALUE = "application/json";
private final HttpMethod mMethod;
private final URLWrapper mUrl;
private final TypeToken<T> mTypeToken;
private final Map<String, String> mPostParameters = new HashMap<String, String>();
private final Map<String, File> mPartParameters = new HashMap<String, File>();
private SendCallback mSendCallback;
@Inject
private Injector mInjector;
@Override
public void setSendCallback(final SendCallback sendCallback) {
mSendCallback = sendCallback;
}
public AbstractSoundcloudApiQueryImpl(final URLWrapper url,
final HttpMethod method, final TypeToken<T> typeToken) {
super();
mMethod = method;
mUrl = url;
mTypeToken = typeToken;
}
@Override
public SoundcloudApiQuery<T> addPostParameter(final String key,
final String value) {
mPostParameters.put(key, value);
return this;
}
@Override
public SoundcloudApiQuery<T> addPartParameters(final String key,
final File file) {
mPartParameters.put(key, file);
return this;
}
@Override
public T execute(final int expected) throws APIException {
final HttpURLConnection connection;
Ln.d("Executing API request for %s.", this.toString());
switch (mMethod) {
case GET:
connection = this.executeGet();
break;
case POST:
connection = this.executePost();
break;
default:
throw new IllegalArgumentException("Method not implemented.");
}
// Don't follow redirects.
connection.setInstanceFollowRedirects(false);
setRequestHeaders(connection);
int code;
try {
code = connection.getResponseCode();
} catch (final IOException e) {
// I consider this a bug. A 401 without auth challenge causes
// an IOException, while it's perfectly valid in terms of RFC 2616.
if (e.getMessage().equals(
"Received authentication challenge is null")) {
code = HttpURLConnection.HTTP_UNAUTHORIZED;
} else {
throw new APIException(e, -1);
}
}
if (code != expected) {
connection.disconnect();
throw new APIException(String.format(
"HTTP request failed with code %d.", code), code);
}
final InputStream responseStream;
try {
responseStream = getWrappedResponseStream(connection);
} catch (final IOException e) {
throw new APIException(e, -1);
}
final String responseStr = convertStreamToString(responseStream);
final Type entityType = mTypeToken.getType();
return new Gson().fromJson(responseStr, entityType);
}
protected abstract void setupPostRequest(HttpRequest request);
protected abstract void setupGetConnection(URLConnection connection);
/**
* Sets custom headers required for all requests.
*
* @param connection
*/
private void setRequestHeaders(final HttpURLConnection connection) {
connection.addRequestProperty(ACCEPT_HEADER_KEY, ACCEPT_HEADER_VALUE);
}
/**
* Executes the POST request. Instead of using the bare HttpURLConnection
* API, we use our modified HttpRequest client to be able to upload with
* multipart and other cool things.
*
* @return
* @throws APIException
*/
protected HttpURLConnection executePost() throws APIException {
Ln.d("Executing POST against URL " + mUrl.toString());
final HttpRequest request = HttpRequest.post(mUrl);
setupPostRequest(request);
if (mSendCallback != null) {
request.setSendCallback(mSendCallback);
}
if (mPartParameters.size() > 0) {
applyPostParametersAsPart(request);
applyPartParameters(request);
} else {
applyPostParameters(request);
}
try {
request.closeOutput();
} catch (final IOException e) {
throw new APIException(e, -1);
}
return request.getConnection();
}
private void applyPostParametersAsPart(final HttpRequest request) {
final Iterator<Entry<String, String>> iterator = mPostParameters
.entrySet().iterator();
while (iterator.hasNext()) {
final Entry<String, String> entry = iterator.next();
Ln.d("Applying POST parameter as multipart: %s", entry.getKey());
request.part(entry.getKey(), entry.getValue());
}
}
/**
* Add multipart parameters to the given HttpRequest.
*
* @param request
*/
private void applyPartParameters(final HttpRequest request) {
final Iterator<Entry<String, File>> iterator = mPartParameters
.entrySet().iterator();
while (iterator.hasNext()) {
final Entry<String, File> entry = iterator.next();
Ln.d("Applying multipart parameter %s.", entry.getKey());
final File file = entry.getValue();
request.part(entry.getKey(), file.getName(), file,
"application/octet-stream");
}
}
/**
* Add POST parameters to the given HttpRequest.
*
* @param request
*/
private void applyPostParameters(final HttpRequest request) {
final Iterator<Entry<String, String>> iterator = mPostParameters
.entrySet().iterator();
while (iterator.hasNext()) {
final Entry<String, String> entry = iterator.next();
final StringBuffer buf = new StringBuffer();
Ln.d("Applying POST parameters: %s", entry.getKey());
buf.append(entry.getKey());
buf.append('=');
buf.append(entry.getValue());
request.send(buf.toString());
}
}
/**
* Executes a GET request and returns the expected Entity.
*
* @param parameters
* @param expected
* @return
* @throws APIException
*/
protected HttpURLConnection executeGet() throws APIException {
Ln.d("Executing GET against URL " + mUrl.toString());
try {
final URLConnection connection = mUrl.openConnection();
setupGetConnection(connection);
return (HttpURLConnection) connection;
} catch (final IOException e) {
throw new APIException(e, -1);
}
}
/**
* Properly wrap the stream accounting for GZIP.
*
* @param is
* Stream to wrap.
* @param gzip
* Whether or not to include a GZIP wrapper.
* @return Wrapped stream.
* @throws IOException
*/
protected static InputStream getWrappedInputStream(final InputStream is,
final boolean gzip) throws IOException {
if (gzip) {
return new BufferedInputStream(new GZIPInputStream(is));
} else {
return new BufferedInputStream(is);
}
}
protected static InputStream getWrappedResponseStream(
final HttpURLConnection response) throws IOException {
return getWrappedInputStream(response.getInputStream(),
GZIP.equalsIgnoreCase(response.getContentEncoding()));
}
/**
* Read an entire stream to end and assemble in a string.
*
* @param is
* Stream to read.
* @return Entire stream contents.
*/
protected static String convertStreamToString(final InputStream is) {
/*
* To convert the InputStream to String we use the
* BufferedReader.readLine() method. We iterate until the BufferedReader
* returns null, which means there's no more data to read. Each line
* will appended to a StringBuilder and returned as String.
*/
final BufferedReader reader = new BufferedReader(new InputStreamReader(
is));
final StringBuilder sb = new StringBuilder();
String line = null;
String newline = System.getProperty("line.seperator");
if (newline == null) {
newline = NEWLINE_FALLBACK;
}
try {
while ((line = reader.readLine()) != null) {
sb.append(line);
sb.append(newline);
}
} catch (final IOException e) {
Ln.e(e);
} finally {
try {
is.close();
} catch (final IOException e) {
Ln.e(e);
}
}
return sb.toString();
}
@Override
public String toString() {
return "SoundcloudApiQuery [method=" + mMethod + ", url=" + mUrl + "]";
}
}