From 43080d2f3c8a36919a45138e14eb65bc5be35137 Mon Sep 17 00:00:00 2001 From: Jan Berkel Date: Sat, 20 Aug 2011 15:01:53 +0200 Subject: [PATCH 001/156] Link to sharing kit --- README.md | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 6c40070..3ad660c 100644 --- a/README.md +++ b/README.md @@ -1,12 +1,19 @@ # java-api-wrapper -OAuth2 SoundCloud API wrapper written in Java ([javadoc][]), extracted from the -[SoundCloud Android][] codebase. +OAuth2 SoundCloud API wrapper written in Java ([javadoc][]). It is simple to use and requires a minimum of external dependencies (compared to the OAuth1 wrapper) so should be easily embeddable in both desktop and mobile applications. +## Android + +The wrapper works well on Android (although it has no dependencies on it), +since it is an extraction from our [SoundCloud Android][] codebase. However, if +all you want is to share sounds from your own application we recommend to check out the +[Android Sharing Kit][] which delegates all the hard work to the SoundCloud +app and makes use of Android's [intent][] framework. + ## Usage Create a wrapper instance: @@ -244,3 +251,6 @@ See LICENSE for details. [snapshots]: https://oss.sonatype.org/content/repositories/snapshots/com/soundcloud/java-api-wrapper/ [releases]: https://oss.sonatype.org/content/repositories/releases/com/soundcloud/java-api-wrapper/ [maven-central]: http://repo1.maven.org/maven2/com/soundcloud/java-api-wrapper/ +[Android Sharing Kit]: https://github.com/soundcloud/android-intent-sharing/wiki +[android-token-sharing]: https://github.com/soundcloud/android-token-sharing +[intent]: http://developer.android.com/reference/android/content/Intent.html From d0865e4b33cc591c0accd1be1c176be47c6d7ef6 Mon Sep 17 00:00:00 2001 From: Jan Berkel Date: Sat, 20 Aug 2011 16:04:01 +0200 Subject: [PATCH 002/156] typo --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 3ad660c..16d939f 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@ mobile applications. ## Android -The wrapper works well on Android (although it has no dependencies on it), +The wrapper works well on Android (although it has no dependencies on it) since it is an extraction from our [SoundCloud Android][] codebase. However, if all you want is to share sounds from your own application we recommend to check out the [Android Sharing Kit][] which delegates all the hard work to the SoundCloud From 09a0f29c0799b14f61be916109f1ec2f05de497c Mon Sep 17 00:00:00 2001 From: Jan Berkel Date: Tue, 23 Aug 2011 13:17:59 +0200 Subject: [PATCH 003/156] Increase the number of maximum connections to the API host --- .../java/com/soundcloud/api/ApiWrapper.java | 53 ++++++++++++++++--- src/main/java/com/soundcloud/api/Env.java | 7 +++ src/main/java/com/soundcloud/api/Http.java | 43 ++------------- src/test/java/com/soundcloud/api/EnvTest.java | 17 ++++++ 4 files changed, 72 insertions(+), 48 deletions(-) create mode 100644 src/test/java/com/soundcloud/api/EnvTest.java diff --git a/src/main/java/com/soundcloud/api/ApiWrapper.java b/src/main/java/com/soundcloud/api/ApiWrapper.java index 8eef9c4..99d4156 100644 --- a/src/main/java/com/soundcloud/api/ApiWrapper.java +++ b/src/main/java/com/soundcloud/api/ApiWrapper.java @@ -22,6 +22,11 @@ import org.apache.http.client.protocol.ClientContext; import org.apache.http.conn.ClientConnectionManager; import org.apache.http.conn.ConnectionKeepAliveStrategy; +import org.apache.http.conn.params.ConnManagerPNames; +import org.apache.http.conn.params.ConnManagerParams; +import org.apache.http.conn.params.ConnPerRoute; +import org.apache.http.conn.params.ConnPerRouteBean; +import org.apache.http.conn.routing.HttpRoute; import org.apache.http.conn.routing.HttpRoutePlanner; import org.apache.http.conn.scheme.PlainSocketFactory; import org.apache.http.conn.scheme.Scheme; @@ -32,6 +37,8 @@ import org.apache.http.impl.client.DefaultRequestDirector; import org.apache.http.impl.conn.tsccm.ThreadSafeClientConnManager; import org.apache.http.message.BasicHeader; +import org.apache.http.params.BasicHttpParams; +import org.apache.http.params.HttpConnectionParams; import org.apache.http.params.HttpParams; import org.apache.http.params.HttpProtocolParams; import org.apache.http.protocol.BasicHttpProcessor; @@ -83,11 +90,18 @@ public class ApiWrapper implements CloudAPI, Serializable { transient private HttpClient httpClient; transient private TokenListener listener; - /** debug request details to stderr */ - public boolean debugRequests; + private String mDefaultContentType; + public static final int BUFFER_SIZE = 8192; + /** Connection timeout */ + public static final int TIMEOUT = 20 * 1000; + /** Keepalive timeout */ + public static final long KEEPALIVE_TIMEOUT = 20 * 1000; + /* maximum number of connections allowed */ + public static final int MAX_TOTAL_CONNECTIONS = 20; - private String mDefaultContentType; + /** debug request details to stderr */ + public boolean debugRequests; /** * Constructs a new ApiWrapper instance. @@ -251,13 +265,36 @@ protected Token requestToken(Request request) throws IOException { } } - - /** - * @return parameters used by the underlying HttpClient + * @return the default HttpParams + * @see + * android.net.http.AndroidHttpClient#newInstance(String, Context) */ protected HttpParams getParams() { - return Http.defaultParams(); + final HttpParams params = new BasicHttpParams(); + HttpConnectionParams.setConnectionTimeout(params, TIMEOUT); + HttpConnectionParams.setSoTimeout(params, TIMEOUT); + HttpConnectionParams.setSocketBufferSize(params, BUFFER_SIZE); + ConnManagerParams.setMaxTotalConnections(params, MAX_TOTAL_CONNECTIONS); + + // Turn off stale checking. Our connections break all the time anyway, + // and it's not worth it to pay the penalty of checking every time. + HttpConnectionParams.setStaleCheckingEnabled(params, false); + + // fix contributed by Bjorn Roche XXX check if still needed + params.setBooleanParameter("http.protocol.expect-continue", false); + params.setParameter(ConnManagerPNames.MAX_CONNECTIONS_PER_ROUTE, new ConnPerRoute() { + @Override + public int getMaxForRoute(HttpRoute httpRoute) { + if (env.isApiHost(httpRoute.getTargetHost())) { + // there will be a lot of concurrent request to the API host + return MAX_TOTAL_CONNECTIONS; + } else { + return ConnPerRouteBean.DEFAULT_MAX_CONNECTIONS_PER_ROUTE; + } + } + }); + return params; } /** @@ -305,7 +342,7 @@ public HttpClient getHttpClient() { setKeepAliveStrategy(new ConnectionKeepAliveStrategy() { @Override public long getKeepAliveDuration(HttpResponse httpResponse, HttpContext httpContext) { - return 20 * 1000; // milliseconds + return KEEPALIVE_TIMEOUT; } }); diff --git a/src/main/java/com/soundcloud/api/Env.java b/src/main/java/com/soundcloud/api/Env.java index 4494af1..25d5b37 100644 --- a/src/main/java/com/soundcloud/api/Env.java +++ b/src/main/java/com/soundcloud/api/Env.java @@ -22,6 +22,7 @@ public enum Env { Env(String resourceHost, String authResourceHost) { this.resourceHost = new HttpHost(resourceHost, -1, "http"); sslResourceHost = new HttpHost(resourceHost, -1, "https"); + this.authResourceHost = new HttpHost(authResourceHost, -1, "http"); sslAuthResourceHost = new HttpHost(authResourceHost, -1, "https"); } @@ -33,4 +34,10 @@ public HttpHost getResourceHost(boolean secure) { public HttpHost getAuthResourceHost(boolean secure) { return secure ? sslAuthResourceHost : authResourceHost; } + + public boolean isApiHost(HttpHost host) { + return ("http".equals(host.getSchemeName()) || + "https".equals(host.getSchemeName())) && + resourceHost.getHostName().equals(host.getHostName()); + } } diff --git a/src/main/java/com/soundcloud/api/Http.java b/src/main/java/com/soundcloud/api/Http.java index fa80579..16a7dcd 100644 --- a/src/main/java/com/soundcloud/api/Http.java +++ b/src/main/java/com/soundcloud/api/Http.java @@ -2,13 +2,6 @@ import org.apache.http.Header; import org.apache.http.HttpResponse; -import org.apache.http.conn.params.ConnManagerPNames; -import org.apache.http.conn.params.ConnPerRoute; -import org.apache.http.conn.params.ConnPerRouteBean; -import org.apache.http.conn.routing.HttpRoute; -import org.apache.http.params.BasicHttpParams; -import org.apache.http.params.HttpConnectionParams; -import org.apache.http.params.HttpParams; import org.apache.http.protocol.HTTP; import org.json.JSONException; import org.json.JSONObject; @@ -20,10 +13,6 @@ * Helper class for various HTTP related functions. */ public class Http { - public static final int BUFFER_SIZE = 8192; - /** Connection timeout */ - public static final int TIMEOUT = 20 * 1000; - private Http() { } @@ -38,7 +27,7 @@ public static String getString(HttpResponse response) throws IOException { InputStream is = response.getEntity().getContent(); if (is == null) return null; - int length = BUFFER_SIZE; + int length = ApiWrapper.BUFFER_SIZE; Header contentLength = null; try { contentLength = response.getFirstHeader(HTTP.CONTENT_LEN); @@ -54,13 +43,13 @@ public static String getString(HttpResponse response) throws IOException { final StringBuilder sb = new StringBuilder(length); int n; - byte[] buffer = new byte[BUFFER_SIZE]; + byte[] buffer = new byte[ApiWrapper.BUFFER_SIZE]; while ((n = is.read(buffer)) != -1) sb.append(new String(buffer, 0, n)); return sb.toString(); } public static JSONObject getJSON(HttpResponse response) throws IOException { - final String json = Http.getString(response); + final String json = getString(response); if (json == null || json.length() == 0) throw new IOException("JSON response is empty"); try { return new JSONObject(json); @@ -69,30 +58,4 @@ public static JSONObject getJSON(HttpResponse response) throws IOException { (json.length() > 80 ? (json.substring(0, 79) + "..." ) : json)); } } - - /** - * @return the default HttpParams - * @see - * android.net.http.AndroidHttpClient#newInstance(String, Context) - */ - public static HttpParams defaultParams() { - final HttpParams params = new BasicHttpParams(); - HttpConnectionParams.setConnectionTimeout(params, TIMEOUT); - HttpConnectionParams.setSoTimeout(params, TIMEOUT); - HttpConnectionParams.setSocketBufferSize(params, 8192); - - // Turn off stale checking. Our connections break all the time anyway, - // and it's not worth it to pay the penalty of checking every time. - HttpConnectionParams.setStaleCheckingEnabled(params, false); - - // fix contributed by Bjorn Roche XXX check if still needed - params.setBooleanParameter("http.protocol.expect-continue", false); - params.setParameter(ConnManagerPNames.MAX_CONNECTIONS_PER_ROUTE, new ConnPerRoute() { - @Override - public int getMaxForRoute(HttpRoute httpRoute) { - return ConnPerRouteBean.DEFAULT_MAX_CONNECTIONS_PER_ROUTE * 3; - } - }); - return params; - } } diff --git a/src/test/java/com/soundcloud/api/EnvTest.java b/src/test/java/com/soundcloud/api/EnvTest.java new file mode 100644 index 0000000..bc47378 --- /dev/null +++ b/src/test/java/com/soundcloud/api/EnvTest.java @@ -0,0 +1,17 @@ +package com.soundcloud.api; + +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +import org.apache.http.HttpHost; +import org.junit.Test; + + +public class EnvTest { + @Test + public void testIsApiHost() throws Exception { + assertTrue(Env.LIVE.isApiHost(new HttpHost("api.soundcloud.com", 80, "http"))); + assertTrue(Env.LIVE.isApiHost(new HttpHost("api.soundcloud.com", 443, "https"))); + assertFalse(Env.LIVE.isApiHost(new HttpHost("foo.soundcloud.com", 443, "https"))); + } +} From 17656480761215dbd78be87c12de6993d72b190e Mon Sep 17 00:00:00 2001 From: Jan Berkel Date: Tue, 23 Aug 2011 13:40:15 +0200 Subject: [PATCH 004/156] Conditional GETs --- src/main/java/com/soundcloud/api/Http.java | 5 +++++ src/main/java/com/soundcloud/api/Request.java | 15 +++++++++++++++ .../soundcloud/api/CloudAPIIntegrationTest.java | 14 ++++++++++++++ 3 files changed, 34 insertions(+) diff --git a/src/main/java/com/soundcloud/api/Http.java b/src/main/java/com/soundcloud/api/Http.java index 16a7dcd..ba1e5b5 100644 --- a/src/main/java/com/soundcloud/api/Http.java +++ b/src/main/java/com/soundcloud/api/Http.java @@ -58,4 +58,9 @@ public static JSONObject getJSON(HttpResponse response) throws IOException { (json.length() > 80 ? (json.substring(0, 79) + "..." ) : json)); } } + + public static String etag(HttpResponse resp) { + Header etag = resp.getFirstHeader("Etag"); + return etag != null ? etag.getValue() : null; + } } diff --git a/src/main/java/com/soundcloud/api/Request.java b/src/main/java/com/soundcloud/api/Request.java index 0f3c1f7..876e296 100644 --- a/src/main/java/com/soundcloud/api/Request.java +++ b/src/main/java/com/soundcloud/api/Request.java @@ -50,6 +50,7 @@ public class Request implements Iterable { private Token mToken; private String mResource; private TransferProgressListener listener; + private String mIfNoneMatch; /** Empty request */ public Request() {} @@ -85,6 +86,7 @@ public Request(Request request) { mToken = request.mToken; listener = request.listener; mParams = new ArrayList(request.mParams); + mIfNoneMatch = request.mIfNoneMatch; if (request.mFiles != null) mFiles = new HashMap(request.mFiles); } @@ -239,6 +241,16 @@ public boolean isMultipart() { (mByteBuffers != null && !mByteBuffers.isEmpty()); } + /** + * Conditional GET + * @param etag the etag to check for (If-None-Match: etag) + * @return this + */ + public Request ifNoneMatch(String etag) { + mIfNoneMatch = etag; + return this; + } + /** * Builds a request with the given set of parameters and files. * @param method the type of request to use @@ -285,6 +297,9 @@ public T buildRequest(Class method) { request.setURI(URI.create(mResource)); } else { // just plain GET/DELETE/... + if (mIfNoneMatch != null) { + request.addHeader("If-None-Match", mIfNoneMatch); + } request.setURI(URI.create(toUrl())); } diff --git a/src/test/java/com/soundcloud/api/CloudAPIIntegrationTest.java b/src/test/java/com/soundcloud/api/CloudAPIIntegrationTest.java index 5c14bb6..6fee7e6 100644 --- a/src/test/java/com/soundcloud/api/CloudAPIIntegrationTest.java +++ b/src/test/java/com/soundcloud/api/CloudAPIIntegrationTest.java @@ -2,6 +2,7 @@ import static org.hamcrest.CoreMatchers.*; import static org.hamcrest.CoreMatchers.containsString; +import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertThat; import org.apache.http.HttpResponse; @@ -170,6 +171,19 @@ public void shouldChangeContentType() throws Exception { containsString("application/xml")); } + + @Test + public void shouldSupportConditionalGets() throws Exception { + login(); + + HttpResponse resp = api.get(Request.to(Endpoints.MY_DETAILS)); + String etag = Http.etag(resp); + assertNotNull(etag); + + resp = api.get(Request.to(Endpoints.MY_DETAILS).ifNoneMatch(etag)); + assertThat(resp.getStatusLine().getStatusCode(), is(304) /* not-modified */); + } + /* @Test public void updateMyDetails() throws Exception { From c16e72e5170ab4b30b92fc55e69893c24743c34a Mon Sep 17 00:00:00 2001 From: Jan Berkel Date: Tue, 23 Aug 2011 14:20:55 +0200 Subject: [PATCH 005/156] Added some test code for concurrent connections --- .../java/com/soundcloud/api/ApiWrapper.java | 2 +- .../api/CloudAPIIntegrationTest.java | 50 +++++++++++++++++-- 2 files changed, 47 insertions(+), 5 deletions(-) diff --git a/src/main/java/com/soundcloud/api/ApiWrapper.java b/src/main/java/com/soundcloud/api/ApiWrapper.java index 99d4156..184017a 100644 --- a/src/main/java/com/soundcloud/api/ApiWrapper.java +++ b/src/main/java/com/soundcloud/api/ApiWrapper.java @@ -98,7 +98,7 @@ public class ApiWrapper implements CloudAPI, Serializable { /** Keepalive timeout */ public static final long KEEPALIVE_TIMEOUT = 20 * 1000; /* maximum number of connections allowed */ - public static final int MAX_TOTAL_CONNECTIONS = 20; + public static final int MAX_TOTAL_CONNECTIONS = 10; /** debug request details to stderr */ public boolean debugRequests; diff --git a/src/test/java/com/soundcloud/api/CloudAPIIntegrationTest.java b/src/test/java/com/soundcloud/api/CloudAPIIntegrationTest.java index 6fee7e6..c96c184 100644 --- a/src/test/java/com/soundcloud/api/CloudAPIIntegrationTest.java +++ b/src/test/java/com/soundcloud/api/CloudAPIIntegrationTest.java @@ -1,13 +1,13 @@ package com.soundcloud.api; import static org.hamcrest.CoreMatchers.*; -import static org.hamcrest.CoreMatchers.containsString; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertThat; import org.apache.http.HttpResponse; import org.json.JSONObject; import org.junit.Before; +import org.junit.Ignore; import org.junit.Test; import java.io.File; @@ -16,6 +16,8 @@ import java.io.IOException; import java.io.InputStream; import java.nio.ByteBuffer; +import java.util.concurrent.BrokenBarrierException; +import java.util.concurrent.CyclicBarrier; public class CloudAPIIntegrationTest implements Params.Track, Endpoints { // http://sandbox-soundcloud.com/you/apps/java-api-wrapper-test-app @@ -184,8 +186,49 @@ public void shouldSupportConditionalGets() throws Exception { assertThat(resp.getStatusLine().getStatusCode(), is(304) /* not-modified */); } - /* - @Test + + @Test @Ignore + public void shouldSupportConcurrentConnectionsToApiHost() throws Exception { + login(); + + int num = 20; + final CyclicBarrier start = new CyclicBarrier(num, new Runnable() { + @Override + public void run() { + System.err.println("starting..."); + } + }); + final CyclicBarrier end = new CyclicBarrier(num); + while (num-- > 0) { + new Thread("t-"+num) { + @Override public void run() { + try { + start.await(); + System.err.println("running: "+toString()); + try { + HttpResponse resp = api.get(Request.to(Endpoints.MY_DETAILS)); + resp.getEntity().consumeContent(); + assertThat(resp.getStatusLine().getStatusCode(), is(200)); + } catch (IOException e) { + throw new RuntimeException(e); + } finally { + System.err.println("finished: "+toString()); + end.await(); + } + } catch (InterruptedException e) { + throw new RuntimeException(e); + } catch (BrokenBarrierException e) { + throw new RuntimeException(e); + } + } + }.start(); + } + start.await(); + end.await(); + System.err.println("all threads finished"); + } + + @Test @Ignore public void updateMyDetails() throws Exception { Request updateMe = Request.to(MY_DETAILS).with( Params.User.WEBSITE, "http://mywebsite.com") @@ -194,7 +237,6 @@ public void updateMyDetails() throws Exception { HttpResponse resp = api.put(updateMe); assertThat(resp.getStatusLine().getStatusCode(), is(200)); } - */ @SuppressWarnings({"UnusedDeclaration"}) private void writeResponse(HttpResponse resp, String file) throws IOException { From 2c398e988a2a672c3d5e0d1d5298e7fd39bafa07 Mon Sep 17 00:00:00 2001 From: Jan Berkel Date: Tue, 23 Aug 2011 14:21:07 +0200 Subject: [PATCH 006/156] changelog --- CHANGES.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/CHANGES.md b/CHANGES.md index 87e5ba3..a497107 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,3 +1,10 @@ +## 1.0.2 2011-08-xx + + * Support for conditional GETs + * Changed the handling of max connections per route + * Added some endpoints + * Added PostResource example + ## 1.0.1 2011-07-04 * Support for non-expiring scope From 7cbb8d7e01455b821779220118831deacea507e3 Mon Sep 17 00:00:00 2001 From: Jan Berkel Date: Thu, 25 Aug 2011 17:53:00 +0200 Subject: [PATCH 007/156] Support for URIs --- src/main/java/com/soundcloud/api/Request.java | 10 ++++++++++ .../java/com/soundcloud/api/RequestTest.java | 16 ++++++++++++++++ 2 files changed, 26 insertions(+) diff --git a/src/main/java/com/soundcloud/api/Request.java b/src/main/java/com/soundcloud/api/Request.java index 876e296..0a90c75 100644 --- a/src/main/java/com/soundcloud/api/Request.java +++ b/src/main/java/com/soundcloud/api/Request.java @@ -20,6 +20,7 @@ import java.net.URI; import java.net.URLDecoder; import java.nio.ByteBuffer; +import java.rmi.server.UnicastRemoteObject; import java.util.ArrayList; import java.util.HashMap; import java.util.Iterator; @@ -78,6 +79,15 @@ public Request(String resource) { } } + /** + * constructs a a request from URI. the hostname+scheme will be ignored + * @param uri - the uri + */ + public Request(URI uri) { + this(uri.getPath() == null ? "/" : uri.getPath() + + (uri.getQuery() == null ? "" : "?"+uri.getQuery())); + } + /** * @param request the request to be copied */ diff --git a/src/test/java/com/soundcloud/api/RequestTest.java b/src/test/java/com/soundcloud/api/RequestTest.java index be719ef..7dc4899 100644 --- a/src/test/java/com/soundcloud/api/RequestTest.java +++ b/src/test/java/com/soundcloud/api/RequestTest.java @@ -21,6 +21,7 @@ import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.File; +import java.net.URI; import java.util.IllegalFormatException; import java.util.Iterator; import java.util.NoSuchElementException; @@ -216,6 +217,21 @@ public void itShouldParseExistingQueryParameters() throws Exception { equalTo("/foo?bar=baz&foo=bar&1=2")); } + @Test + public void itShouldParseFullURI() throws Exception { + assertThat( + new Request(URI.create("http://foo.soundcloud.com/foo?bar=baz")).with("1", "2").toUrl(), + equalTo("/foo?bar=baz&1=2")); + + assertThat( + new Request(URI.create("http://foo.soundcloud.com/foo")).with("1", "2").toUrl(), + equalTo("/foo?1=2")); + + assertThat( + new Request(URI.create("http://foo.soundcloud.com/")).toUrl(), + equalTo("/")); + } + @Test public void shouldHaveCopyConstructor() { Request orig = new Request("/foo").with("1", 2, "3",4); From 353a27f9064d9c67547f15f6cfce3aae858c8142 Mon Sep 17 00:00:00 2001 From: Jan Berkel Date: Thu, 25 Aug 2011 19:21:35 +0200 Subject: [PATCH 008/156] toString() --- src/main/java/com/soundcloud/api/Request.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/java/com/soundcloud/api/Request.java b/src/main/java/com/soundcloud/api/Request.java index 0a90c75..3687cdb 100644 --- a/src/main/java/com/soundcloud/api/Request.java +++ b/src/main/java/com/soundcloud/api/Request.java @@ -334,11 +334,11 @@ public T buildRequest(Class method) { @Override public String toString() { return "Request{" + - "params=" + mParams + + "mResource='" + mResource + '\'' + + ", params=" + mParams + ", files=" + mFiles + ", entity=" + mEntity + ", mToken=" + mToken + - ", mResource='" + mResource + '\'' + ", listener=" + listener + '}'; } From ff554bcee4402f848627cd5f2b9cb78968bc6c09 Mon Sep 17 00:00:00 2001 From: Jan Berkel Date: Mon, 29 Aug 2011 17:31:08 +0200 Subject: [PATCH 009/156] assert 200 --- src/test/java/com/soundcloud/api/CloudAPIIntegrationTest.java | 1 + 1 file changed, 1 insertion(+) diff --git a/src/test/java/com/soundcloud/api/CloudAPIIntegrationTest.java b/src/test/java/com/soundcloud/api/CloudAPIIntegrationTest.java index c96c184..0335b02 100644 --- a/src/test/java/com/soundcloud/api/CloudAPIIntegrationTest.java +++ b/src/test/java/com/soundcloud/api/CloudAPIIntegrationTest.java @@ -179,6 +179,7 @@ public void shouldSupportConditionalGets() throws Exception { login(); HttpResponse resp = api.get(Request.to(Endpoints.MY_DETAILS)); + assertThat(resp.getStatusLine().getStatusCode(), is(200) /* ok */); String etag = Http.etag(resp); assertNotNull(etag); From d8b0d1c0fc7a19791fbb1b9bfa1f98e4ddcd88d1 Mon Sep 17 00:00:00 2001 From: Jan Berkel Date: Mon, 29 Aug 2011 18:37:40 +0200 Subject: [PATCH 010/156] fix test --- src/test/java/com/soundcloud/api/RequestTest.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/test/java/com/soundcloud/api/RequestTest.java b/src/test/java/com/soundcloud/api/RequestTest.java index 7dc4899..afdb874 100644 --- a/src/test/java/com/soundcloud/api/RequestTest.java +++ b/src/test/java/com/soundcloud/api/RequestTest.java @@ -199,7 +199,7 @@ public void shouldThrowIllegalFormatExceptionWhenInvalidParameters() throws Exce public void toStringShouldWork() throws Exception { assertThat( new Request("/foo").with("1", "2").toString(), - equalTo("Request{params=[1=2], files=null, entity=null, mToken=null, mResource='/foo', listener=null}")); + equalTo("Request{mResource='/foo', params=[1=2], files=null, entity=null, mToken=null, listener=null}")); } @Test From a26876e435b5a07ba830232df02cb52ad6418091 Mon Sep 17 00:00:00 2001 From: Jan Berkel Date: Sun, 11 Sep 2011 14:33:38 +0200 Subject: [PATCH 011/156] code formatting --- README.md | 30 ++++++++++++++++++++---------- 1 file changed, 20 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index 16d939f..de1918b 100644 --- a/README.md +++ b/README.md @@ -18,24 +18,32 @@ app and makes use of Android's [intent][] framework. Create a wrapper instance: - ApiWrapper wrapper = new ApiWrapper("client_id", "client_secret", - null, null, Env.LIVE); +``` +ApiWrapper wrapper = new ApiWrapper("client_id", "client_secret", + null, null, Env.LIVE); +``` Obtain a token: - wrapper.login("username", "password"); +``` +wrapper.login("username", "password"); +``` Execute a request: - HttpResponse resp = wrapper.get(Request.to("/me")); +``` +HttpResponse resp = wrapper.get(Request.to("/me")); +``` Update a resource: - HttpResponse resp = - wrapper.put(Request.to("/me") - .with("user[full_name]", "Che Flute", - "user[website]", "http://cheflute.com") - .withFile("user[avatar_data]", new File("flute.jpg"))); +``` +HttpResponse resp = + wrapper.put(Request.to("/me") + .with("user[full_name]", "Che Flute", + "user[website]", "http://cheflute.com") + .withFile("user[avatar_data]", new File("flute.jpg"))); +``` ## Migrating from OAuth1 @@ -62,7 +70,9 @@ the authentication process. If you don't want to use them you can request non-expiring tokens by specifying the scope "non-expiring" when exchanging the tokens: - Token token = wrapper.login("username", "password", Token.SCOPE_NON_EXPIRING); +``` +Token token = wrapper.login("username", "password", Token.SCOPE_NON_EXPIRING); +``` The resulting token will be valid until revoked manually. From ec3e774700816343220fdc4b526900b947b29e41 Mon Sep 17 00:00:00 2001 From: Jan Berkel Date: Sun, 11 Sep 2011 14:34:35 +0200 Subject: [PATCH 012/156] code --- README.md | 2 +- build.gradle | 4 ++-- src/main/java/com/soundcloud/api/Request.java | 4 ++++ src/test/java/com/soundcloud/api/RequestTest.java | 5 +++++ 4 files changed, 12 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index de1918b..113b736 100644 --- a/README.md +++ b/README.md @@ -18,7 +18,7 @@ app and makes use of Android's [intent][] framework. Create a wrapper instance: -``` +```java ApiWrapper wrapper = new ApiWrapper("client_id", "client_secret", null, null, Env.LIVE); ``` diff --git a/build.gradle b/build.gradle index 5c425a1..1a77bb7 100644 --- a/build.gradle +++ b/build.gradle @@ -16,8 +16,8 @@ sourceSets { } dependencies { - compile group: 'org.apache.httpcomponents', name: 'httpclient', version: '4.0.3' - compile group: 'org.apache.httpcomponents', name: 'httpmime', version: '4.0.3' + compile group: 'org.apache.httpcomponents', name: 'httpclient', version: '4.1.2' + compile group: 'org.apache.httpcomponents', name: 'httpmime', version: '4.1.2' compile group: 'org.json', name: 'json', version: '20090211' testCompile group: 'junit', name: 'junit-dep', version: '4.8.2' diff --git a/src/main/java/com/soundcloud/api/Request.java b/src/main/java/com/soundcloud/api/Request.java index 3687cdb..120a911 100644 --- a/src/main/java/com/soundcloud/api/Request.java +++ b/src/main/java/com/soundcloud/api/Request.java @@ -155,6 +155,10 @@ public int size() { return mParams.size(); } + public String getResource() { + return mResource; + } + /** * @return a String that is suitable for use as an application/x-www-form-urlencoded * list of parameters in an HTTP PUT or HTTP POST. diff --git a/src/test/java/com/soundcloud/api/RequestTest.java b/src/test/java/com/soundcloud/api/RequestTest.java index afdb874..bace8d6 100644 --- a/src/test/java/com/soundcloud/api/RequestTest.java +++ b/src/test/java/com/soundcloud/api/RequestTest.java @@ -253,4 +253,9 @@ public void shouldNotModifyOriginal() { assertThat(copy.getToken(), not(equalTo(orig.getToken()))); assertThat(orig.getListener(),equalTo(copy.getListener())); } + + @Test + public void shouldExposeResource() throws Exception { + assertThat(new Request("/foo?foo=bar").getResource(), equalTo("/foo")); + } } From 7e55ca080e95e1b1961c6204407592113d16d4e2 Mon Sep 17 00:00:00 2001 From: Jan Berkel Date: Sun, 11 Sep 2011 14:35:05 +0200 Subject: [PATCH 013/156] code --- README.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 113b736..e9b851b 100644 --- a/README.md +++ b/README.md @@ -25,19 +25,19 @@ ApiWrapper wrapper = new ApiWrapper("client_id", "client_secret", Obtain a token: -``` +```java wrapper.login("username", "password"); ``` Execute a request: -``` +```java HttpResponse resp = wrapper.get(Request.to("/me")); ``` Update a resource: -``` +```java HttpResponse resp = wrapper.put(Request.to("/me") .with("user[full_name]", "Che Flute", @@ -70,7 +70,7 @@ the authentication process. If you don't want to use them you can request non-expiring tokens by specifying the scope "non-expiring" when exchanging the tokens: -``` +```java Token token = wrapper.login("username", "password", Token.SCOPE_NON_EXPIRING); ``` From 12d7d55815c8b5ab4ebb534647698eba8b4f4fc7 Mon Sep 17 00:00:00 2001 From: Jan Berkel Date: Sun, 11 Sep 2011 14:36:43 +0200 Subject: [PATCH 014/156] code --- README.md | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index e9b851b..7ba380d 100644 --- a/README.md +++ b/README.md @@ -50,7 +50,9 @@ HttpResponse resp = If your app uses OAuth1 and already has users with access tokens you can easily migrate to OAuth2 without requiring anybody to reauthenticate: - Token token = wrapper.exchangeOAuth1Token("validoauth1token"); +```java +Token token = wrapper.exchangeOAuth1Token("validoauth1token"); +``` Note that this is specific to SoundCloud and not part of the current OAuth2 draft. @@ -78,8 +80,10 @@ The resulting token will be valid until revoked manually. For the `authorization_code` grant type you need to request the scope like so: - URI uri = wrapper.authorizationCodeUrl(Endpoints.CONNECT, Token.SCOPE_NON_EXPIRING); - // open uri in browser / WebView etc. +```java +URI uri = wrapper.authorizationCodeUrl(Endpoints.CONNECT, Token.SCOPE_NON_EXPIRING); +// open uri in browser / WebView etc. +``` ## Requirements From 878ff4a2e0ddc83fa3f7774727534ee77f089937 Mon Sep 17 00:00:00 2001 From: Jan Berkel Date: Wed, 28 Sep 2011 13:39:53 +0200 Subject: [PATCH 015/156] Added support for specifying upload filenames --- src/main/java/com/soundcloud/api/Request.java | 115 +++++++++++++++--- .../java/com/soundcloud/api/RequestTest.java | 38 +++++- 2 files changed, 133 insertions(+), 20 deletions(-) diff --git a/src/main/java/com/soundcloud/api/Request.java b/src/main/java/com/soundcloud/api/Request.java index 120a911..66fddd2 100644 --- a/src/main/java/com/soundcloud/api/Request.java +++ b/src/main/java/com/soundcloud/api/Request.java @@ -9,6 +9,7 @@ import org.apache.http.entity.mime.MIME; import org.apache.http.entity.mime.MultipartEntity; import org.apache.http.entity.mime.content.AbstractContentBody; +import org.apache.http.entity.mime.content.ContentBody; import org.apache.http.entity.mime.content.FileBody; import org.apache.http.entity.mime.content.StringBody; import org.apache.http.message.BasicNameValuePair; @@ -20,7 +21,6 @@ import java.net.URI; import java.net.URLDecoder; import java.nio.ByteBuffer; -import java.rmi.server.UnicastRemoteObject; import java.util.ArrayList; import java.util.HashMap; import java.util.Iterator; @@ -44,8 +44,8 @@ */ public class Request implements Iterable { private List mParams = new ArrayList(); // XXX should probably be lazy - private Map mFiles; - private Map mByteBuffers; + private Map mFiles; + private HttpEntity mEntity; private Token mToken; @@ -97,7 +97,7 @@ public Request(Request request) { listener = request.listener; mParams = new ArrayList(request.mParams); mIfNoneMatch = request.mIfNoneMatch; - if (request.mFiles != null) mFiles = new HashMap(request.mFiles); + if (request.mFiles != null) mFiles = new HashMap(request.mFiles); } /** @@ -186,8 +186,19 @@ public String toUrl() { * @return this */ public Request withFile(String name, File file) { - if (mFiles == null) mFiles = new HashMap(); - if (file != null) mFiles.put(name, file); + return withFile(name, file, file.getName()); + } + + /** + * Registers a file to be uploaded with a POST or PUT request. + * @param name the name of the parameter + * @param file the file to be submitted + * @param fileName the name of the uploaded file (overrides file parameter) + * @return this + */ + public Request withFile(String name, File file, String fileName) { + if (mFiles == null) mFiles = new HashMap(); + if (file != null) mFiles.put(name, new Attachment(file, fileName)); return this; } @@ -198,7 +209,18 @@ public Request withFile(String name, File file) { * @return this */ public Request withFile(String name, byte[] data) { - return withFile(name, ByteBuffer.wrap(data)); + return withFile(name, ByteBuffer.wrap(data), null); + } + + /** + * Registers binary data to be uploaded with a POST or PUT request. + * @param name the name of the parameter + * @param data the data to be submitted + * @param fileName the name of the uploaded file + * @return this + */ + public Request withFile(String name, byte[] data, String fileName) { + return withFile(name, ByteBuffer.wrap(data), fileName); } /** @@ -208,8 +230,20 @@ public Request withFile(String name, byte[] data) { * @return this */ public Request withFile(String name, ByteBuffer data) { - if (mByteBuffers == null) mByteBuffers = new HashMap(); - if (data != null) mByteBuffers.put(name, data); + return withFile(name, data, null); + } + + + /** + * Registers binary data to be uploaded with a POST or PUT request. + * @param name the name of the parameter + * @param data the data to be submitted + * @param fileName the name of the uploaded file + * @return this + */ + public Request withFile(String name, ByteBuffer data, String fileName) { + if (mFiles == null) mFiles = new HashMap(); + if (data != null) mFiles.put(name, new Attachment(data, fileName)); return this; } @@ -251,8 +285,7 @@ public Request setProgressListener(TransferProgressListener listener) { } public boolean isMultipart() { - return (mFiles != null && !mFiles.isEmpty()) || - (mByteBuffers != null && !mByteBuffers.isEmpty()); + return mFiles != null && !mFiles.isEmpty(); } /** @@ -283,14 +316,8 @@ public T buildRequest(Class method) { MultipartEntity multiPart = new MultipartEntity(); if (mFiles != null) { - for (Map.Entry e : mFiles.entrySet()) { - multiPart.addPart(e.getKey(), new FileBody(e.getValue())); - } - } - - if (mByteBuffers != null) { - for (Map.Entry e : mByteBuffers.entrySet()) { - multiPart.addPart(e.getKey(), new ByteBufferBody(e.getValue())); + for (Map.Entry e : mFiles.entrySet()) { + multiPart.addPart(e.getKey(), e.getValue().toContentBody()); } } @@ -420,4 +447,54 @@ public void writeTo(OutputStream out) throws IOException { } } } + + /* package */ static class Attachment { + public final File file; + public final ByteBuffer data; + public final String fileName; + + Attachment(File file) { + this(file, file.getName()); + } + + Attachment(File file, String fileName) { + if (file == null) throw new IllegalArgumentException("file cannot be null"); + this.fileName = fileName; + this.file = file; + this.data = null; + } + + Attachment(ByteBuffer data) { + this(data, null); + } + + Attachment(ByteBuffer data, String fileName) { + if (data == null) throw new IllegalArgumentException("data cannot be null"); + + this.data = data; + this.fileName = fileName; + this.file = null; + } + + public ContentBody toContentBody() { + if (file != null) { + return new FileBody(file) { + @Override + public String getFilename() { + return fileName; + } + }; + } else if (data != null) { + return new ByteBufferBody(data) { + @Override + public String getFilename() { + return fileName; + } + }; + } else { + // never happens + throw new IllegalStateException("no upload data"); + } + } + } } diff --git a/src/test/java/com/soundcloud/api/RequestTest.java b/src/test/java/com/soundcloud/api/RequestTest.java index bace8d6..f4e1331 100644 --- a/src/test/java/com/soundcloud/api/RequestTest.java +++ b/src/test/java/com/soundcloud/api/RequestTest.java @@ -117,7 +117,28 @@ public void shouldCreateMultipartRequestWhenFilesAreAdded() throws Exception { assertThat(encoded, containsString("foo")); assertThat(encoded, containsString("key")); assertThat(encoded, containsString("value")); - assertThat(encoded, containsString("testing")); + assertThat(encoded, containsString("filename=\"testing")); + } + + @Test + public void shouldOverrideFilenameInUpload() throws Exception { + File f = File.createTempFile("testing", "test"); + + HttpPost request = Request.to("/foo") + .with("key", "value") + .withFile("foo", f, "music.mp3") + .buildRequest(HttpPost.class); + + assertTrue(request.getEntity() instanceof MultipartEntity); + + ByteArrayOutputStream os = new ByteArrayOutputStream(); + request.getEntity().writeTo(os); + String encoded = os.toString(); + + assertThat(encoded, containsString("foo")); + assertThat(encoded, containsString("key")); + assertThat(encoded, containsString("value")); + assertThat(encoded, containsString("filename=\"music.mp3\"")); } @Test @@ -130,6 +151,21 @@ public void shouldDetectMultipartRequests() throws Exception { .withFile("foo", "foo".getBytes()).isMultipart()); } + @Test + public void shouldUploadByteDataWithFilename() throws Exception { + HttpPost request = Request.to("/foo") + .with("key", "value") + .withFile("testing", "foo".getBytes(), "music.mp3") + .buildRequest(HttpPost.class); + + assertTrue(request.getEntity() instanceof MultipartEntity); + + ByteArrayOutputStream os = new ByteArrayOutputStream(); + request.getEntity().writeTo(os); + String encoded = os.toString(); + assertThat(encoded, containsString("filename=\"music.mp3\"")); + } + @Test public void shouldCreateMultipartRequestWhenFilesAreAddedWithByteArray() throws Exception { HttpPost request = Request.to("/foo") From 780f5e1d148ed053fe07d1b6792a3f90a8f946df Mon Sep 17 00:00:00 2001 From: Jan Berkel Date: Wed, 28 Sep 2011 13:42:50 +0200 Subject: [PATCH 016/156] Revert "code" This reverts commit ec3e774700816343220fdc4b526900b947b29e41. --- README.md | 2 +- build.gradle | 4 ++-- src/main/java/com/soundcloud/api/Request.java | 4 ---- src/test/java/com/soundcloud/api/RequestTest.java | 5 ----- 4 files changed, 3 insertions(+), 12 deletions(-) diff --git a/README.md b/README.md index 7ba380d..7f617a1 100644 --- a/README.md +++ b/README.md @@ -18,7 +18,7 @@ app and makes use of Android's [intent][] framework. Create a wrapper instance: -```java +``` ApiWrapper wrapper = new ApiWrapper("client_id", "client_secret", null, null, Env.LIVE); ``` diff --git a/build.gradle b/build.gradle index 1a77bb7..5c425a1 100644 --- a/build.gradle +++ b/build.gradle @@ -16,8 +16,8 @@ sourceSets { } dependencies { - compile group: 'org.apache.httpcomponents', name: 'httpclient', version: '4.1.2' - compile group: 'org.apache.httpcomponents', name: 'httpmime', version: '4.1.2' + compile group: 'org.apache.httpcomponents', name: 'httpclient', version: '4.0.3' + compile group: 'org.apache.httpcomponents', name: 'httpmime', version: '4.0.3' compile group: 'org.json', name: 'json', version: '20090211' testCompile group: 'junit', name: 'junit-dep', version: '4.8.2' diff --git a/src/main/java/com/soundcloud/api/Request.java b/src/main/java/com/soundcloud/api/Request.java index 66fddd2..cc0791e 100644 --- a/src/main/java/com/soundcloud/api/Request.java +++ b/src/main/java/com/soundcloud/api/Request.java @@ -155,10 +155,6 @@ public int size() { return mParams.size(); } - public String getResource() { - return mResource; - } - /** * @return a String that is suitable for use as an application/x-www-form-urlencoded * list of parameters in an HTTP PUT or HTTP POST. diff --git a/src/test/java/com/soundcloud/api/RequestTest.java b/src/test/java/com/soundcloud/api/RequestTest.java index f4e1331..ebb9bef 100644 --- a/src/test/java/com/soundcloud/api/RequestTest.java +++ b/src/test/java/com/soundcloud/api/RequestTest.java @@ -289,9 +289,4 @@ public void shouldNotModifyOriginal() { assertThat(copy.getToken(), not(equalTo(orig.getToken()))); assertThat(orig.getListener(),equalTo(copy.getListener())); } - - @Test - public void shouldExposeResource() throws Exception { - assertThat(new Request("/foo?foo=bar").getResource(), equalTo("/foo")); - } } From 788cc67cd15a2da4b69d6e904d3b5d0b5f6ebb8e Mon Sep 17 00:00:00 2001 From: Jan Berkel Date: Wed, 28 Sep 2011 13:44:50 +0200 Subject: [PATCH 017/156] Java formatting --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 7f617a1..7ba380d 100644 --- a/README.md +++ b/README.md @@ -18,7 +18,7 @@ app and makes use of Android's [intent][] framework. Create a wrapper instance: -``` +```java ApiWrapper wrapper = new ApiWrapper("client_id", "client_secret", null, null, Env.LIVE); ``` From 23c1d6eac56dd6b01b949ea29aaf9e2dd5d7aa34 Mon Sep 17 00:00:00 2001 From: Jan Berkel Date: Wed, 28 Sep 2011 16:21:16 +0200 Subject: [PATCH 018/156] Use HttpMultipartMode.BROWSER_COMPATIBLE This is needed because of broken multipart decoding on the API side (fixed here: https://github.com/rack/rack/commit/acffe8ef5ea6de74fe306f2dd908b7681a21aaad) [Closes #2] --- build.gradle | 4 +++ src/main/java/com/soundcloud/api/Request.java | 33 +++++++++---------- .../api/CloudAPIIntegrationTest.java | 2 +- 3 files changed, 20 insertions(+), 19 deletions(-) diff --git a/build.gradle b/build.gradle index 5c425a1..ecbed9c 100644 --- a/build.gradle +++ b/build.gradle @@ -116,3 +116,7 @@ def getAuth(repo_id) { } [:] } + +task printDebug << { + println httpDebug.collect { "-D"+it.key+"="+it.value }.join(' ') +} diff --git a/src/main/java/com/soundcloud/api/Request.java b/src/main/java/com/soundcloud/api/Request.java index cc0791e..73f4963 100644 --- a/src/main/java/com/soundcloud/api/Request.java +++ b/src/main/java/com/soundcloud/api/Request.java @@ -6,6 +6,7 @@ import org.apache.http.client.methods.HttpRequestBase; import org.apache.http.client.utils.URLEncodedUtils; import org.apache.http.entity.StringEntity; +import org.apache.http.entity.mime.HttpMultipartMode; import org.apache.http.entity.mime.MIME; import org.apache.http.entity.mime.MultipartEntity; import org.apache.http.entity.mime.content.AbstractContentBody; @@ -13,6 +14,7 @@ import org.apache.http.entity.mime.content.FileBody; import org.apache.http.entity.mime.content.StringBody; import org.apache.http.message.BasicNameValuePair; +import org.apache.james.mime4j.util.CharsetUtil; import java.io.File; import java.io.IOException; @@ -21,6 +23,7 @@ import java.net.URI; import java.net.URLDecoder; import java.nio.ByteBuffer; +import java.nio.charset.Charset; import java.util.ArrayList; import java.util.HashMap; import java.util.Iterator; @@ -202,10 +205,11 @@ public Request withFile(String name, File file, String fileName) { * Registers binary data to be uploaded with a POST or PUT request. * @param name the name of the parameter * @param data the data to be submitted + * @deprecated use {@link #withFile(String, byte[], String)} instead * @return this */ public Request withFile(String name, byte[] data) { - return withFile(name, ByteBuffer.wrap(data), null); + return withFile(name, ByteBuffer.wrap(data)); } /** @@ -224,9 +228,10 @@ public Request withFile(String name, byte[] data, String fileName) { * @param name the name of the parameter * @param data the data to be submitted * @return this + * @deprecated use {@link #withFile(String, java.nio.ByteBuffer), String} instead */ public Request withFile(String name, ByteBuffer data) { - return withFile(name, data, null); + return withFile(name, data, "upload"); } @@ -308,8 +313,12 @@ public T buildRequest(Class method) { HttpEntityEnclosingRequestBase enclosingRequest = (HttpEntityEnclosingRequestBase) request; + final Charset charSet = CharsetUtil.getCharset("UTF-8"); if (isMultipart()) { - MultipartEntity multiPart = new MultipartEntity(); + MultipartEntity multiPart = new MultipartEntity( + HttpMultipartMode.BROWSER_COMPATIBLE, // XXX change this to STRICT once rack on server is upgraded + null, + charSet); if (mFiles != null) { for (Map.Entry e : mFiles.entrySet()) { @@ -318,7 +327,7 @@ public T buildRequest(Class method) { } for (NameValuePair pair : mParams) { - multiPart.addPart(pair.getName(), new StringBodyNoHeaders(pair.getValue())); + multiPart.addPart(pair.getName(), new StringBody(pair.getValue(), "text/plain", charSet)); } enclosingRequest.setEntity(listener == null ? multiPart : @@ -392,20 +401,6 @@ public static interface TransferProgressListener { - static class StringBodyNoHeaders extends StringBody { - public StringBodyNoHeaders(String value) throws UnsupportedEncodingException { - super(value); - } - - @Override public String getMimeType() { - return null; - } - - @Override public String getTransferEncoding() { - return null; - } - } - static class ByteBufferBody extends AbstractContentBody { private ByteBuffer mBuffer; @@ -449,6 +444,7 @@ public void writeTo(OutputStream out) throws IOException { public final ByteBuffer data; public final String fileName; + /** @noinspection UnusedDeclaration*/ Attachment(File file) { this(file, file.getName()); } @@ -460,6 +456,7 @@ public void writeTo(OutputStream out) throws IOException { this.data = null; } + /** @noinspection UnusedDeclaration*/ Attachment(ByteBuffer data) { this(data, null); } diff --git a/src/test/java/com/soundcloud/api/CloudAPIIntegrationTest.java b/src/test/java/com/soundcloud/api/CloudAPIIntegrationTest.java index 0335b02..568b879 100644 --- a/src/test/java/com/soundcloud/api/CloudAPIIntegrationTest.java +++ b/src/test/java/com/soundcloud/api/CloudAPIIntegrationTest.java @@ -76,7 +76,7 @@ public void shouldUploadASimpleAudioFileBytes() throws Exception { HttpResponse resp = api.post(Request.to(TRACKS).with( TITLE, "Hello Android", POST_TO_EMPTY, "") - .withFile(ASSET_DATA, bb)); + .withFile(ASSET_DATA, bb, "hello.aiff")); int status = resp.getStatusLine().getStatusCode(); assertThat(status, is(201)); From 1a56e2b0e3f603c26432048711365ae98a408121 Mon Sep 17 00:00:00 2001 From: Jan Berkel Date: Wed, 28 Sep 2011 16:25:42 +0200 Subject: [PATCH 019/156] Changelog --- CHANGES.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGES.md b/CHANGES.md index a497107..3b10921 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,5 +1,6 @@ ## 1.0.2 2011-08-xx + * Support httpmime-4.1.x (GH-2) * Support for conditional GETs * Changed the handling of max connections per route * Added some endpoints From c7401e7ed33badc4f7cba3d1c51219e27caf77d3 Mon Sep 17 00:00:00 2001 From: Jan Berkel Date: Wed, 28 Sep 2011 17:08:42 +0200 Subject: [PATCH 020/156] Check for file --- src/main/java/com/soundcloud/api/Request.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/com/soundcloud/api/Request.java b/src/main/java/com/soundcloud/api/Request.java index 73f4963..5796e9f 100644 --- a/src/main/java/com/soundcloud/api/Request.java +++ b/src/main/java/com/soundcloud/api/Request.java @@ -185,7 +185,7 @@ public String toUrl() { * @return this */ public Request withFile(String name, File file) { - return withFile(name, file, file.getName()); + return file != null ? withFile(name, file, file.getName()) : this; } /** From ca46dd5e4f2aed13f782f427f8182592047c1c95 Mon Sep 17 00:00:00 2001 From: Jon Schmidt Date: Mon, 10 Oct 2011 12:37:54 +0200 Subject: [PATCH 021/156] updated params to include ids --- src/main/java/com/soundcloud/api/Params.java | 1 + 1 file changed, 1 insertion(+) diff --git a/src/main/java/com/soundcloud/api/Params.java b/src/main/java/com/soundcloud/api/Params.java index 8e596b6..ad10d28 100644 --- a/src/main/java/com/soundcloud/api/Params.java +++ b/src/main/java/com/soundcloud/api/Params.java @@ -35,6 +35,7 @@ interface Track { String BPM = "track[bpm]"; String LICENSE = "track[license]"; String SHARED_EMAILS = "track[shared_to][emails][][address]"; + String SHARED_IDS = "track[shared_to][users][][id]"; String SHARING_NOTE = "track[sharing_note]"; String PUBLIC = "public"; String PRIVATE = "private"; From 48d8c0c491a0b8d505f2f93ba4b701972f2d789e Mon Sep 17 00:00:00 2001 From: Jan Berkel Date: Tue, 11 Oct 2011 15:52:44 +0200 Subject: [PATCH 022/156] Support for range requests --- src/main/java/com/soundcloud/api/Request.java | 24 ++++++++++++ .../java/com/soundcloud/api/RequestTest.java | 38 +++++++++++++++++++ 2 files changed, 62 insertions(+) diff --git a/src/main/java/com/soundcloud/api/Request.java b/src/main/java/com/soundcloud/api/Request.java index 5796e9f..be4e519 100644 --- a/src/main/java/com/soundcloud/api/Request.java +++ b/src/main/java/com/soundcloud/api/Request.java @@ -55,6 +55,7 @@ public class Request implements Iterable { private String mResource; private TransferProgressListener listener; private String mIfNoneMatch; + private long[] mRange; /** Empty request */ public Request() {} @@ -276,6 +277,11 @@ public Request withContent(String content, String contentType) { } } + public Request range(long... ranges) { + mRange = ranges; + return this; + } + /** * @param listener a listener for receiving notifications about transfer progress * @return this @@ -343,6 +349,10 @@ public T buildRequest(Class method) { request.setURI(URI.create(mResource)); } else { // just plain GET/DELETE/... + if (mRange != null) { + request.addHeader("Range", formatRange(mRange)); + } + if (mIfNoneMatch != null) { request.addHeader("If-None-Match", mIfNoneMatch); } @@ -363,6 +373,20 @@ public T buildRequest(Class method) { } } + static String formatRange(long... range) { + switch (range.length) { + case 0: return "bytes=0-"; + case 1: + if (range[0] < 0) throw new IllegalArgumentException("negative range"); + return "bytes="+range[0]+"-"; + case 2: + if (range[0] < 0) throw new IllegalArgumentException("negative range"); + if (range[0] > range[1]) throw new IllegalArgumentException(range[0] + ">" + range[1]); + return "bytes="+range[0]+"-"+range[1]; + default: throw new IllegalArgumentException("invalid range specified"); + } + } + @Override public Iterator iterator() { return mParams.iterator(); } diff --git a/src/test/java/com/soundcloud/api/RequestTest.java b/src/test/java/com/soundcloud/api/RequestTest.java index ebb9bef..e261c96 100644 --- a/src/test/java/com/soundcloud/api/RequestTest.java +++ b/src/test/java/com/soundcloud/api/RequestTest.java @@ -100,6 +100,17 @@ public void shouldAddTokenToHeaderIfSpecified() throws Exception { assertThat(auth.getValue(), CoreMatchers.containsString("acc3ss")); } + @Test + public void shouldAddRangeHeaderIfSpecified() throws Exception { + HttpGet request = Request.to("/foo") + .range(1,200) + .buildRequest(HttpGet.class); + + Header auth = request.getFirstHeader("Range"); + assertNotNull(auth); + assertThat(auth.getValue(), equalTo("bytes=1-200")); + } + @Test public void shouldCreateMultipartRequestWhenFilesAreAdded() throws Exception { File f = File.createTempFile("testing", "test"); @@ -289,4 +300,31 @@ public void shouldNotModifyOriginal() { assertThat(copy.getToken(), not(equalTo(orig.getToken()))); assertThat(orig.getListener(),equalTo(copy.getListener())); } + + @Test + public void testFormatRange() throws Exception { + assertThat(Request.formatRange(1, 1000), equalTo("bytes=1-1000")); + assertThat(Request.formatRange(1), equalTo("bytes=1-")); + assertThat(Request.formatRange(), equalTo("bytes=0-")); + } + + @Test(expected = IllegalArgumentException.class) + public void testFormatRangeInvalidArgument() throws Exception { + Request.formatRange(100,200,300); + } + + @Test(expected = IllegalArgumentException.class) + public void testFormatRangeInvalidArgument2() throws Exception { + Request.formatRange(1000, 1); + } + + @Test(expected = IllegalArgumentException.class) + public void testFormatRangeInvalidArgument3() throws Exception { + Request.formatRange(-1); + } + + @Test(expected = IllegalArgumentException.class) + public void testFormatRangeInvalidArgument4() throws Exception { + Request.formatRange(-1, 200); + } } From 192b28cfe2cd7dd8216438a67eaf5920bd0502b3 Mon Sep 17 00:00:00 2001 From: Jan Berkel Date: Tue, 11 Oct 2011 16:39:20 +0200 Subject: [PATCH 023/156] Added some more tests --- CHANGES.md | 3 ++ .../soundcloud/api/examples/GetResource.java | 14 ++------- .../soundcloud/api/examples/PostResource.java | 15 +-------- .../soundcloud/api/examples/PutResource.java | 14 +-------- .../java/com/soundcloud/api/ApiWrapper.java | 11 +++++++ .../java/com/soundcloud/api/CloudAPI.java | 18 +++++++++++ src/main/java/com/soundcloud/api/Http.java | 13 ++++++++ src/main/java/com/soundcloud/api/Request.java | 4 +-- .../com/soundcloud/api/ApiWrapperTest.java | 1 + .../api/CloudAPIIntegrationTest.java | 31 +++++++++++++++++++ 10 files changed, 83 insertions(+), 41 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 3b10921..f55a8d5 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -2,6 +2,9 @@ * Support httpmime-4.1.x (GH-2) * Support for conditional GETs + * Support for Range requests + * added CloudApi#resolveStreamUrl(String) + * added CloudApi#getHttpClient() * Changed the handling of max connections per route * Added some endpoints * Added PostResource example diff --git a/src/examples/java/com/soundcloud/api/examples/GetResource.java b/src/examples/java/com/soundcloud/api/examples/GetResource.java index 353dfc5..056c2f0 100644 --- a/src/examples/java/com/soundcloud/api/examples/GetResource.java +++ b/src/examples/java/com/soundcloud/api/examples/GetResource.java @@ -36,7 +36,7 @@ public static void main(String[] args) throws Exception { try { HttpResponse resp = wrapper.get(resource); if (resp.getStatusLine().getStatusCode() == HttpStatus.SC_OK) { - System.out.println("\n" + formatJSON(Http.getString(resp))); + System.out.println("\n" + Http.formatJSON(Http.getString(resp))); } else { System.err.println("Invalid status received: " + resp.getStatusLine()); } @@ -47,15 +47,5 @@ public static void main(String[] args) throws Exception { } } - static String formatJSON(String s) { - try { - return new JSONObject(s).toString(4); - } catch (JSONException e) { - try { - return new JSONArray(s).toString(4); - } catch (JSONException e2) { - return s; - } - } - } + } diff --git a/src/examples/java/com/soundcloud/api/examples/PostResource.java b/src/examples/java/com/soundcloud/api/examples/PostResource.java index d1ccaca..a5bb634 100644 --- a/src/examples/java/com/soundcloud/api/examples/PostResource.java +++ b/src/examples/java/com/soundcloud/api/examples/PostResource.java @@ -34,7 +34,7 @@ public static void main(String[] args) throws Exception { try { HttpResponse resp = wrapper.post(resource); if (resp.getStatusLine().getStatusCode() == HttpStatus.SC_CREATED) { - System.out.println("\n" + formatJSON(Http.getString(resp))); + System.out.println("\n" + Http.formatJSON(Http.getString(resp))); } else { System.err.println("Invalid status received: " + resp.getStatusLine()); } @@ -44,17 +44,4 @@ public static void main(String[] args) throws Exception { } } } - - - static String formatJSON(String s) { - try { - return new JSONObject(s).toString(4); - } catch (JSONException e) { - try { - return new JSONArray(s).toString(4); - } catch (JSONException e2) { - return s; - } - } - } } diff --git a/src/examples/java/com/soundcloud/api/examples/PutResource.java b/src/examples/java/com/soundcloud/api/examples/PutResource.java index 2059536..65a2163 100644 --- a/src/examples/java/com/soundcloud/api/examples/PutResource.java +++ b/src/examples/java/com/soundcloud/api/examples/PutResource.java @@ -40,7 +40,7 @@ public static void main(String[] args) throws Exception { try { HttpResponse resp = wrapper.put(resource); if (resp.getStatusLine().getStatusCode() == HttpStatus.SC_OK) { - System.out.println("\n" + formatJSON(Http.getString(resp))); + System.out.println("\n" + Http.formatJSON(Http.getString(resp))); } else { System.err.println("Invalid status received: " + resp.getStatusLine()); } @@ -50,16 +50,4 @@ public static void main(String[] args) throws Exception { } } } - - static String formatJSON(String s) { - try { - return new JSONObject(s).toString(4); - } catch (JSONException e) { - try { - return new JSONArray(s).toString(4); - } catch (JSONException e2) { - return s; - } - } - } } diff --git a/src/main/java/com/soundcloud/api/ApiWrapper.java b/src/main/java/com/soundcloud/api/ApiWrapper.java index 184017a..7492cfb 100644 --- a/src/main/java/com/soundcloud/api/ApiWrapper.java +++ b/src/main/java/com/soundcloud/api/ApiWrapper.java @@ -406,6 +406,17 @@ public long resolve(String url) throws IOException { return -1; } + @Override + public String resolveStreamUrl(String url) throws IOException { + HttpResponse resp = get(Request.to(url)); + if (resp.getStatusLine().getStatusCode() == HttpStatus.SC_MOVED_TEMPORARILY) { + Header location = resp.getFirstHeader("Location"); + return (location != null) ? location.getValue() : null; + } else { + return null; + } + } + @Override public HttpResponse get(Request request) throws IOException { return execute(request, HttpGet.class); } diff --git a/src/main/java/com/soundcloud/api/CloudAPI.java b/src/main/java/com/soundcloud/api/CloudAPI.java index cb015be..bf77cbc 100644 --- a/src/main/java/com/soundcloud/api/CloudAPI.java +++ b/src/main/java/com/soundcloud/api/CloudAPI.java @@ -1,6 +1,8 @@ package com.soundcloud.api; +import org.apache.http.HttpRequest; import org.apache.http.HttpResponse; +import org.apache.http.client.HttpClient; import java.io.IOException; import java.net.URI; @@ -163,6 +165,11 @@ public interface CloudAPI { */ HttpResponse delete(Request request) throws IOException; + /** + * @return the used httpclient + */ + HttpClient getHttpClient(); + /** * Resolve the given SoundCloud URI * @@ -172,6 +179,17 @@ public interface CloudAPI { */ long resolve(String uri) throws IOException; + + /** + * Resolve the given SoundCloud stream URI + * + * @param uri SoundCloud stream URI, e.g. https://api.soundcloud.com/tracks/25272620/stream + * @return the resolved url or null if not possible + * @throws IOException network errors + */ + String resolveStreamUrl(String uri) throws IOException; + + /** @return the current token */ Token getToken(); diff --git a/src/main/java/com/soundcloud/api/Http.java b/src/main/java/com/soundcloud/api/Http.java index ba1e5b5..3fda503 100644 --- a/src/main/java/com/soundcloud/api/Http.java +++ b/src/main/java/com/soundcloud/api/Http.java @@ -3,6 +3,7 @@ import org.apache.http.Header; import org.apache.http.HttpResponse; import org.apache.http.protocol.HTTP; +import org.json.JSONArray; import org.json.JSONException; import org.json.JSONObject; @@ -63,4 +64,16 @@ public static String etag(HttpResponse resp) { Header etag = resp.getFirstHeader("Etag"); return etag != null ? etag.getValue() : null; } + + public static String formatJSON(String s) { + try { + return new JSONObject(s).toString(4); + } catch (JSONException e) { + try { + return new JSONArray(s).toString(4); + } catch (JSONException e2) { + return s; + } + } + } } diff --git a/src/main/java/com/soundcloud/api/Request.java b/src/main/java/com/soundcloud/api/Request.java index be4e519..ecfcdcd 100644 --- a/src/main/java/com/soundcloud/api/Request.java +++ b/src/main/java/com/soundcloud/api/Request.java @@ -209,7 +209,7 @@ public Request withFile(String name, File file, String fileName) { * @deprecated use {@link #withFile(String, byte[], String)} instead * @return this */ - public Request withFile(String name, byte[] data) { + @Deprecated public Request withFile(String name, byte[] data) { return withFile(name, ByteBuffer.wrap(data)); } @@ -231,7 +231,7 @@ public Request withFile(String name, byte[] data, String fileName) { * @return this * @deprecated use {@link #withFile(String, java.nio.ByteBuffer), String} instead */ - public Request withFile(String name, ByteBuffer data) { + @Deprecated public Request withFile(String name, ByteBuffer data) { return withFile(name, data, "upload"); } diff --git a/src/test/java/com/soundcloud/api/ApiWrapperTest.java b/src/test/java/com/soundcloud/api/ApiWrapperTest.java index 1414978..64fa782 100644 --- a/src/test/java/com/soundcloud/api/ApiWrapperTest.java +++ b/src/test/java/com/soundcloud/api/ApiWrapperTest.java @@ -43,6 +43,7 @@ public class ApiWrapperTest { @Before public void setup() { api = new ApiWrapper("invalid", "invalid", URI.create("redirect://me"), null, Env.SANDBOX) { + private static final long serialVersionUID = 12345; // silence warnings @Override protected RequestDirector getRequestDirector(HttpRequestExecutor requestExec, ClientConnectionManager conman, diff --git a/src/test/java/com/soundcloud/api/CloudAPIIntegrationTest.java b/src/test/java/com/soundcloud/api/CloudAPIIntegrationTest.java index 568b879..41dba11 100644 --- a/src/test/java/com/soundcloud/api/CloudAPIIntegrationTest.java +++ b/src/test/java/com/soundcloud/api/CloudAPIIntegrationTest.java @@ -4,12 +4,15 @@ import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertThat; +import org.apache.http.Header; import org.apache.http.HttpResponse; +import org.apache.http.client.methods.HttpGet; import org.json.JSONObject; import org.junit.Before; import org.junit.Ignore; import org.junit.Test; +import javax.xml.ws.Endpoint; import java.io.File; import java.io.FileInputStream; import java.io.FileOutputStream; @@ -122,6 +125,34 @@ public void shouldResolveUrls() throws Exception { assertThat(id, is(1862213L)); } + @Test + public void shouldResolveStreamUrls() throws Exception { + login(); + + String resolved = api.resolveStreamUrl("https://api.sandbox-soundcloud.com/tracks/2100832/stream"); + + assertThat(resolved, not(nullValue())); + assertThat(resolved, containsString("http://ak-media.soundcloud.com/")); + } + + @Test + public void shouldSupportRangeRequest() throws Exception { + login(); + + String resolved = api.resolveStreamUrl("https://api.sandbox-soundcloud.com/tracks/2100832/stream"); + assertThat(resolved, not(nullValue())); + + HttpResponse resp = api + .getHttpClient() + .execute(Request.to(resolved).range(50, 100).buildRequest(HttpGet.class)); + + assertThat(resp.getStatusLine().toString(), resp.getStatusLine().getStatusCode(), is(206)); + Header range = resp.getFirstHeader("Content-Range"); + assertThat(range, notNullValue()); + assertThat(range.getValue(), equalTo("bytes 50-100/19643")); + assertThat(resp.getEntity().getContentLength(), is(51L)); + } + @Test public void readMyDetails() throws Exception { login(); From d7566b8b552d19240aabbfbb2b25b92198b1b34e Mon Sep 17 00:00:00 2001 From: Jan Berkel Date: Tue, 11 Oct 2011 16:44:21 +0200 Subject: [PATCH 024/156] import --- src/test/java/com/soundcloud/api/CloudAPIIntegrationTest.java | 1 - 1 file changed, 1 deletion(-) diff --git a/src/test/java/com/soundcloud/api/CloudAPIIntegrationTest.java b/src/test/java/com/soundcloud/api/CloudAPIIntegrationTest.java index 41dba11..f7ae7ec 100644 --- a/src/test/java/com/soundcloud/api/CloudAPIIntegrationTest.java +++ b/src/test/java/com/soundcloud/api/CloudAPIIntegrationTest.java @@ -12,7 +12,6 @@ import org.junit.Ignore; import org.junit.Test; -import javax.xml.ws.Endpoint; import java.io.File; import java.io.FileInputStream; import java.io.FileOutputStream; From 720bd567a515e86e953d1200a7a5774279ceea1e Mon Sep 17 00:00:00 2001 From: Jan Berkel Date: Tue, 25 Oct 2011 15:53:01 +0200 Subject: [PATCH 025/156] Added stream resolving to wrapper --- CHANGES.md | 2 + .../java/com/soundcloud/api/ApiWrapper.java | 42 +++++- .../java/com/soundcloud/api/CloudAPI.java | 42 +++++- src/main/java/com/soundcloud/api/Request.java | 2 +- src/main/java/com/soundcloud/api/Stream.java | 134 ++++++++++++++++++ .../api/CloudAPIIntegrationTest.java | 39 ++++- .../java/com/soundcloud/api/RequestTest.java | 5 + .../java/com/soundcloud/api/StreamTest.java | 71 ++++++++++ .../com/soundcloud/api/s3-headers.txt | 15 ++ 9 files changed, 332 insertions(+), 20 deletions(-) create mode 100644 src/main/java/com/soundcloud/api/Stream.java create mode 100644 src/test/java/com/soundcloud/api/StreamTest.java create mode 100644 src/test/resources/com/soundcloud/api/s3-headers.txt diff --git a/CHANGES.md b/CHANGES.md index f55a8d5..764cae7 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -8,6 +8,8 @@ * Changed the handling of max connections per route * Added some endpoints * Added PostResource example + * Added support for HEAD requests + * Added stream resolving ## 1.0.1 2011-07-04 diff --git a/src/main/java/com/soundcloud/api/ApiWrapper.java b/src/main/java/com/soundcloud/api/ApiWrapper.java index 7492cfb..5929e8e 100644 --- a/src/main/java/com/soundcloud/api/ApiWrapper.java +++ b/src/main/java/com/soundcloud/api/ApiWrapper.java @@ -15,6 +15,7 @@ import org.apache.http.client.UserTokenHandler; import org.apache.http.client.methods.HttpDelete; import org.apache.http.client.methods.HttpGet; +import org.apache.http.client.methods.HttpHead; import org.apache.http.client.methods.HttpPost; import org.apache.http.client.methods.HttpPut; import org.apache.http.client.methods.HttpRequestBase; @@ -397,26 +398,53 @@ public long resolve(String url) throws IOException { if (s.contains("/")) { try { return Integer.parseInt(s.substring(s.lastIndexOf("/") + 1, s.length())); - } catch (NumberFormatException ignored) { - // ignored + } catch (NumberFormatException e) { + throw new ResolverException(e, resp); } + } else { + throw new ResolverException("Invalid string:"+s, resp); } + } else { + throw new ResolverException("No location header", resp); } + } else { + throw new ResolverException("Invalid status code", resp); } - return -1; } @Override - public String resolveStreamUrl(String url) throws IOException { - HttpResponse resp = get(Request.to(url)); + public Stream resolveStreamUrl(String url) throws IOException { + HttpResponse resp = head(Request.to(url)); if (resp.getStatusLine().getStatusCode() == HttpStatus.SC_MOVED_TEMPORARILY) { Header location = resp.getFirstHeader("Location"); - return (location != null) ? location.getValue() : null; + if (location != null && location.getValue() != null) { + final String headRedirect = location.getValue(); + resp = getHttpClient().execute(new HttpHead(headRedirect)); + if (resp.getStatusLine().getStatusCode() == HttpStatus.SC_OK) { + Stream stream = new Stream(url, headRedirect, resp); + // need to do another GET request to have a URL ready for client usage + resp = get(Request.to(url)); + if (resp.getStatusLine().getStatusCode() == HttpStatus.SC_MOVED_TEMPORARILY) { + return stream.withNewStreamUrl(resp.getFirstHeader("Location").getValue()); + } else { + throw new ResolverException("Unexpected response code", resp); + } + } else { + throw new ResolverException("Unexpected response code", resp); + } + } else { + throw new ResolverException("Location header not set", resp); + } } else { - return null; + throw new ResolverException("Unexpected response code", resp); } } + @Override + public HttpResponse head(Request request) throws IOException { + return execute(request, HttpHead.class); + } + @Override public HttpResponse get(Request request) throws IOException { return execute(request, HttpGet.class); } diff --git a/src/main/java/com/soundcloud/api/CloudAPI.java b/src/main/java/com/soundcloud/api/CloudAPI.java index bf77cbc..fe4a4ca 100644 --- a/src/main/java/com/soundcloud/api/CloudAPI.java +++ b/src/main/java/com/soundcloud/api/CloudAPI.java @@ -1,6 +1,5 @@ package com.soundcloud.api; -import org.apache.http.HttpRequest; import org.apache.http.HttpResponse; import org.apache.http.client.HttpClient; @@ -137,6 +136,13 @@ public interface CloudAPI { */ Token invalidateToken(); + /** + * @param request resource to HEAD + * @return the HTTP response + * @throws IOException IO/Error + */ + HttpResponse head(Request request) throws IOException; + /** * @param request resource to GET * @return the HTTP response @@ -174,21 +180,21 @@ public interface CloudAPI { * Resolve the given SoundCloud URI * * @param uri SoundCloud model URI, e.g. http://soundcloud.com/bob - * @return the id or -1 if uri not found + * @return the id * @throws IOException network errors + * @throws ResolverException if object could not be resolved */ long resolve(String uri) throws IOException; - /** * Resolve the given SoundCloud stream URI * * @param uri SoundCloud stream URI, e.g. https://api.soundcloud.com/tracks/25272620/stream - * @return the resolved url or null if not possible + * @return the resolved stream * @throws IOException network errors + * @throws com.soundcloud.api.CloudAPI.ResolverException resolver error (invalid status etc) */ - String resolveStreamUrl(String uri) throws IOException; - + Stream resolveStreamUrl(String uri) throws IOException; /** @return the current token */ Token getToken(); @@ -259,4 +265,28 @@ public InvalidTokenException(int code, String status) { super("HTTP error:" + code + " (" + status + ")"); } } + + class ResolverException extends IOException { + private static final long serialVersionUID = -2990651725862868387L; + + public final HttpResponse response; + public ResolverException(String s, HttpResponse resp) { + super(s); + this.response = resp; + } + + public ResolverException(Throwable throwable, HttpResponse response) { + super(throwable); + this.response = response; + } + + public int getStatusCode() { + return response.getStatusLine().getStatusCode(); + } + + @Override + public String getMessage() { + return super.getMessage()+" "+(response != null ? response.getStatusLine() : ""); + } + } } diff --git a/src/main/java/com/soundcloud/api/Request.java b/src/main/java/com/soundcloud/api/Request.java index ecfcdcd..c1a6ba7 100644 --- a/src/main/java/com/soundcloud/api/Request.java +++ b/src/main/java/com/soundcloud/api/Request.java @@ -348,7 +348,7 @@ public T buildRequest(Class method) { } request.setURI(URI.create(mResource)); - } else { // just plain GET/DELETE/... + } else { // just plain GET/HEAD/DELETE/... if (mRange != null) { request.addHeader("Range", formatRange(mRange)); } diff --git a/src/main/java/com/soundcloud/api/Stream.java b/src/main/java/com/soundcloud/api/Stream.java new file mode 100644 index 0000000..8b36699 --- /dev/null +++ b/src/main/java/com/soundcloud/api/Stream.java @@ -0,0 +1,134 @@ +package com.soundcloud.api; + +import org.apache.http.Header; +import org.apache.http.HttpResponse; +import org.apache.http.impl.cookie.DateUtils; + +import java.io.UnsupportedEncodingException; +import java.net.URLDecoder; +import java.text.DateFormat; +import java.text.ParseException; +import java.text.SimpleDateFormat; +import java.util.Locale; + +/** + * Class representing a remote audio stream object, including metadata. + */ +public class Stream { + static final String AMZ_BITRATE = "x-amz-meta-bitrate"; + static final String AMZ_DURATION = "x-amz-meta-duration"; + static final String EXPIRES = "Expires"; + + public static final long DEFAULT_URL_LIFETIME = 60 * 1000; // expire after 1 minute + public static final DateFormat DATE_FORMAT = new SimpleDateFormat(DateUtils.PATTERN_RFC1123, Locale.US); + + public final String url; + public final String streamUrl; + public final String eTag; + public final long contentLength; + public final long lastModified; + public final int duration; + public final int bitRate; + public final long expires; + + public Stream(String url, String streamUrl, HttpResponse resp) throws CloudAPI.ResolverException { + this(url, streamUrl, getHeaderValue(resp, "ETag"), + getLongHeader(resp, "Content-Length"), + getDateHeader(resp, "Last-Modified"), + getIntHeader(resp, AMZ_DURATION), + getIntHeader(resp, AMZ_BITRATE), + getExpires(streamUrl)); + } + + public Stream(String url, String streamUrl, String eTag, long contentLength, long lastModified, + int duration, int bitRate, long expires) { + this.url = url; + this.streamUrl = streamUrl; + this.eTag = eTag; + this.contentLength = contentLength; + this.lastModified = lastModified; + this.duration = duration; + this.bitRate = bitRate; + this.expires = expires; + } + + public Request streamUrl() { + return Request.to(streamUrl); + } + + public Request url() { + return Request.to(url); + } + + public Stream withNewStreamUrl(String newStreamUrl) { + return new Stream(url, newStreamUrl, eTag, contentLength, lastModified, duration, bitRate, getExpires(newStreamUrl)); + } + + public static long getLongHeader(HttpResponse resp, String name) throws CloudAPI.ResolverException { + try { + return Long.parseLong(getHeaderValue(resp, name)); + } catch (NumberFormatException e) { + throw new CloudAPI.ResolverException(e, resp); + } + } + + public static int getIntHeader(HttpResponse resp, String name) throws CloudAPI.ResolverException { + try { + return Integer.parseInt(getHeaderValue(resp, name)); + } catch (NumberFormatException e) { + throw new CloudAPI.ResolverException(e, resp); + } + } + + public static long getDateHeader(HttpResponse resp, String name) throws CloudAPI.ResolverException { + try { + return DATE_FORMAT.parse(getHeaderValue(resp, name)).getTime(); + } catch (ParseException e) { + throw new CloudAPI.ResolverException(e, resp); + } + } + + private static String getHeaderValue(HttpResponse resp, String name) throws CloudAPI.ResolverException { + Header h = resp.getFirstHeader(name); + if (h != null && h.getValue() != null) { + return h.getValue(); + } else { + throw new CloudAPI.ResolverException("header " + name + " not set", resp); + } + } + + private static long getExpires(String resource) { + String query = resource.substring(Math.min(resource.length(), resource.indexOf("?")+1), + resource.length()); + for (String s : query.split("&")) { + String[] kv = s.split("=", 2); + if (kv != null && kv.length == 2) { + try { + String name = URLDecoder.decode(kv[0], "UTF-8"); + if (EXPIRES.equalsIgnoreCase(name)) { + String value = URLDecoder.decode(kv[1], "UTF-8"); + try { + return Long.parseLong(value) * 1000L; + } catch (NumberFormatException ignored) { + } + } + } catch (UnsupportedEncodingException ignored) {} + } + } + return System.currentTimeMillis() + DEFAULT_URL_LIFETIME; + } + + @Override + public String toString() { + return "Stream{" + + "url='" + url + '\'' + + ", streamUrl='" + streamUrl + '\'' + + ", eTag='" + eTag + '\'' + + ", contentLength=" + contentLength + + ", lastModified=" + lastModified + + ", duration=" + duration + + ", bitRate=" + bitRate + + ", expires=" + expires + + '}'; + } +} diff --git a/src/test/java/com/soundcloud/api/CloudAPIIntegrationTest.java b/src/test/java/com/soundcloud/api/CloudAPIIntegrationTest.java index f7ae7ec..5ed7747 100644 --- a/src/test/java/com/soundcloud/api/CloudAPIIntegrationTest.java +++ b/src/test/java/com/soundcloud/api/CloudAPIIntegrationTest.java @@ -1,12 +1,16 @@ package com.soundcloud.api; +import static junit.framework.Assert.assertTrue; import static org.hamcrest.CoreMatchers.*; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertThat; +import static org.junit.Assert.fail; +import junit.framework.Assert; import org.apache.http.Header; import org.apache.http.HttpResponse; import org.apache.http.client.methods.HttpGet; +import org.hamcrest.CoreMatchers; import org.json.JSONObject; import org.junit.Before; import org.junit.Ignore; @@ -122,28 +126,51 @@ public void shouldResolveUrls() throws Exception { long id = api.resolve("http://sandbox-soundcloud.com/api-testing"); assertThat(id, is(1862213L)); + + try { + id = api.resolve("http://sandbox-soundcloud.com/i-do-no-exist"); + fail("expected resolver exception, got: "+id); + } catch (CloudAPI.ResolverException e) { + // expected + assertThat(e.getStatusCode(), is(404)); + } } @Test public void shouldResolveStreamUrls() throws Exception { login(); - String resolved = api.resolveStreamUrl("https://api.sandbox-soundcloud.com/tracks/2100832/stream"); + Stream resolved = api.resolveStreamUrl("https://api.sandbox-soundcloud.com/tracks/2100832/stream"); + + assertThat(resolved.url, equalTo("https://api.sandbox-soundcloud.com/tracks/2100832/stream")); + assertThat(resolved.streamUrl, containsString("http://ak-media.soundcloud.com/")); + + assertTrue("expire should be in the future", resolved.expires > System.currentTimeMillis()); + assertThat(resolved.eTag, equalTo("\"1298a3c38b12dc055ad0f7beb956bc56\"")); + } - assertThat(resolved, not(nullValue())); - assertThat(resolved, containsString("http://ak-media.soundcloud.com/")); + @Test + public void shouldThrowResolverExceptionWhenStreamCannotBeResolved() throws Exception { + login(); + try { + Stream s = api.resolveStreamUrl("https://api.sandbox-soundcloud.com/tracks/999919191/stream"); + fail("expected resolver exception, got: "+s); + } catch (CloudAPI.ResolverException e) { + // expected + assertThat(e.getStatusCode(), is(404)); + } } @Test public void shouldSupportRangeRequest() throws Exception { login(); - String resolved = api.resolveStreamUrl("https://api.sandbox-soundcloud.com/tracks/2100832/stream"); - assertThat(resolved, not(nullValue())); + Stream resolved = api.resolveStreamUrl("https://api.sandbox-soundcloud.com/tracks/2100832/stream"); + assertThat(resolved.contentLength, is(19643L)); HttpResponse resp = api .getHttpClient() - .execute(Request.to(resolved).range(50, 100).buildRequest(HttpGet.class)); + .execute(resolved.streamUrl().range(50, 100).buildRequest(HttpGet.class)); assertThat(resp.getStatusLine().toString(), resp.getStatusLine().getStatusCode(), is(206)); Header range = resp.getFirstHeader("Content-Range"); diff --git a/src/test/java/com/soundcloud/api/RequestTest.java b/src/test/java/com/soundcloud/api/RequestTest.java index e261c96..5a1e171 100644 --- a/src/test/java/com/soundcloud/api/RequestTest.java +++ b/src/test/java/com/soundcloud/api/RequestTest.java @@ -262,6 +262,11 @@ public void itShouldParseExistingQueryParameters() throws Exception { assertThat( new Request("/foo?bar=baz&foo=bar").with("1", "2").toUrl(), equalTo("/foo?bar=baz&foo=bar&1=2")); + + String s3 = "http://ak-media.soundcloud.com/XAGeEabPextR.128.mp3?AWSAccessKeyId=AKIAJBHW5FB4ERKUQUOQ&Expires=1319547723&Signature=o53ozj2b%2BrdARFBEZoAziK7mWIY%3D&__gda__=1319547723_e7e8d73cf3af2b003d891ecc01c20143"; + + assertThat(Request.to(s3).toUrl(), equalTo(s3)); + } @Test diff --git a/src/test/java/com/soundcloud/api/StreamTest.java b/src/test/java/com/soundcloud/api/StreamTest.java new file mode 100644 index 0000000..24ee677 --- /dev/null +++ b/src/test/java/com/soundcloud/api/StreamTest.java @@ -0,0 +1,71 @@ +package com.soundcloud.api; + +import static org.hamcrest.CoreMatchers.equalTo; +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.CoreMatchers.not; +import static org.junit.Assert.assertThat; +import static org.junit.Assert.assertTrue; + +import org.apache.http.HttpException; +import org.apache.http.HttpResponse; +import org.apache.http.impl.DefaultHttpResponseFactory; +import org.apache.http.impl.io.AbstractSessionInputBuffer; +import org.apache.http.impl.io.HttpResponseParser; +import org.apache.http.message.BasicLineParser; +import org.apache.http.params.BasicHttpParams; +import org.apache.http.params.HttpParams; +import org.junit.Test; + +import java.io.IOException; + +public class StreamTest { + @Test + public void testParsing() throws Exception { + Stream s = new Stream( + "http://api.soundcloud.com", + "http://ak-media.soundcloud.com/Nbhil06qjDaP.128.mp3?AWSAccessKeyId=AKIAJBHW5FB4ERKUQUOQ&Expires=1319537336&Signature=tzk9EAm3bcjpMJ0cukHPdVx2ml4%3D&__gda__=1319537336_9354e7fea41da4f7a87e78db9a4ed582", + parse("s3-headers.txt")); + + assertThat("etag", s.eTag, equalTo("\"81c4a04a366ab681ea068b2fa06d10a3\"")); + assertThat("bitrate", s.bitRate, is(128)); + assertThat("duration", s.duration, is(18998)); + assertThat("content-length", s.contentLength, is(303855L)); + assertThat("last-modified", s.lastModified, is(1319536883000L)); + assertThat("expires", s.expires, is(1319537336000L)); + } + + @Test + public void testWithNewStreamUrl() throws Exception { + Stream s1 = new Stream( + "http://api.soundcloud.com", + "http://ak-media.soundcloud.com/Nbhil06qjDaP.128.mp3?AWSAccessKeyId=AKIAJBHW5FB4ERKUQUOQ&Expires=1319537336&Signature=tzk9EAm3bcjpMJ0cukHPdVx2ml4%3D&__gda__=1319537336_9354e7fea41da4f7a87e78db9a4ed582", + parse("s3-headers.txt")); + + Stream s2 = s1.withNewStreamUrl("http://ak-media.soundcloud.com/Nbhil06qjDaP.128.mp3?AWSAccessKeyId=AKIAJBHW5FB4ERKUQUOQ&Expires=1319537337&Signature=tzk9EAm3bcjpMJ0cukHPdVx2ml4%3D&__gda__=1319537336_9354e7fea41da4f7a87e78db9a4ed582"); + + assertThat(s1.eTag, equalTo(s2.eTag)); + assertThat(s1.bitRate, equalTo(s2.bitRate)); + assertThat(s1.duration, equalTo(s2.duration)); + assertThat(s1.contentLength, equalTo(s2.contentLength)); + assertThat(s1.lastModified, equalTo(s2.lastModified)); + + assertThat(s1.streamUrl, not(equalTo(s2.streamUrl))); + assertThat(s1.expires, not(is(s2.expires))); + } + + private HttpResponse parse(final String resource) throws IOException, HttpException { + final HttpParams params = new BasicHttpParams(); + HttpResponseParser parser = new HttpResponseParser(new AbstractSessionInputBuffer() { + { + init(getClass().getResourceAsStream(resource), 8192, params); + } + + @Override + public boolean isDataAvailable(int timeout) throws IOException { + return true; + } + }, new BasicLineParser(), new DefaultHttpResponseFactory(), params); + + return (HttpResponse) parser.parse(); + } +} diff --git a/src/test/resources/com/soundcloud/api/s3-headers.txt b/src/test/resources/com/soundcloud/api/s3-headers.txt new file mode 100644 index 0000000..f5842f9 --- /dev/null +++ b/src/test/resources/com/soundcloud/api/s3-headers.txt @@ -0,0 +1,15 @@ +HTTP/1.1 200 OK +x-amz-id-2: 7WKzm/D/jJXIB52DBYCcs57p44C1W67H+zPP+D/UnETmZfcVtaswkvtyQegBoPY/ +x-amz-request-id: 39B41EBA20F893D8 +x-amz-meta-bitrate: 128 +x-amz-meta-duration: 18998 +x-amz-meta-job: Nbhil06qjDaP +Last-Modified: Tue, 25 Oct 2011 10:01:23 GMT +ETag: "81c4a04a366ab681ea068b2fa06d10a3" +Accept-Ranges: bytes +Content-Type: audio/mpeg +Content-Length: 303855 +Server: AmazonS3 +Cache-Control: max-age=31535935 +Date: Tue, 25 Oct 2011 10:06:45 GMT +Connection: keep-alive \ No newline at end of file From 382799afa64a5015097801eebd7a48a936f8a8d2 Mon Sep 17 00:00:00 2001 From: Jan Berkel Date: Tue, 25 Oct 2011 15:58:27 +0200 Subject: [PATCH 026/156] Bump version number --- CHANGES.md | 2 +- build.gradle | 2 +- pom.xml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 764cae7..49f1e67 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,4 +1,4 @@ -## 1.0.2 2011-08-xx +## 1.1.0-SNAPSHOT 2011-08-xx * Support httpmime-4.1.x (GH-2) * Support for conditional GETs diff --git a/build.gradle b/build.gradle index ecbed9c..28c82ce 100644 --- a/build.gradle +++ b/build.gradle @@ -4,7 +4,7 @@ apply plugin: 'eclipse' apply plugin: 'maven' sourceCompatibility = 1.6 -version = '1.0.2-SNAPSHOT' +version = '1.1.0-SNAPSHOT' group = 'com.soundcloud' repositories { mavenCentral() } diff --git a/pom.xml b/pom.xml index 308424a..6f0443e 100644 --- a/pom.xml +++ b/pom.xml @@ -3,7 +3,7 @@ 4.0.0 com.soundcloud java-api-wrapper - 1.0.2-SNAPSHOT + 1.1.0-SNAPSHOT SoundCloud Java API wrapper SoundCloud Java API wrapper (OAuth2 only), works on Android https://github.com/soundcloud/java-api-wrapper#readme From 0fd72321334cc7543246878cf6934dea4a3c5dbd Mon Sep 17 00:00:00 2001 From: Jan Berkel Date: Tue, 1 Nov 2011 15:13:30 +0100 Subject: [PATCH 027/156] Stream should be serializable --- src/main/java/com/soundcloud/api/Stream.java | 9 ++++-- .../java/com/soundcloud/api/StreamTest.java | 29 +++++++++++++++++++ 2 files changed, 35 insertions(+), 3 deletions(-) diff --git a/src/main/java/com/soundcloud/api/Stream.java b/src/main/java/com/soundcloud/api/Stream.java index 8b36699..cab576f 100644 --- a/src/main/java/com/soundcloud/api/Stream.java +++ b/src/main/java/com/soundcloud/api/Stream.java @@ -4,6 +4,7 @@ import org.apache.http.HttpResponse; import org.apache.http.impl.cookie.DateUtils; +import java.io.Serializable; import java.io.UnsupportedEncodingException; import java.net.URLDecoder; import java.text.DateFormat; @@ -14,14 +15,16 @@ /** * Class representing a remote audio stream object, including metadata. */ -public class Stream { - static final String AMZ_BITRATE = "x-amz-meta-bitrate"; - static final String AMZ_DURATION = "x-amz-meta-duration"; +public class Stream implements Serializable { + public static final String AMZ_BITRATE = "x-amz-meta-bitrate"; + public static final String AMZ_DURATION = "x-amz-meta-duration"; static final String EXPIRES = "Expires"; public static final long DEFAULT_URL_LIFETIME = 60 * 1000; // expire after 1 minute public static final DateFormat DATE_FORMAT = new SimpleDateFormat(DateUtils.PATTERN_RFC1123, Locale.US); + private static final long serialVersionUID = -2054788615389851590L; + public final String url; public final String streamUrl; public final String eTag; diff --git a/src/test/java/com/soundcloud/api/StreamTest.java b/src/test/java/com/soundcloud/api/StreamTest.java index 24ee677..1650aa1 100644 --- a/src/test/java/com/soundcloud/api/StreamTest.java +++ b/src/test/java/com/soundcloud/api/StreamTest.java @@ -16,7 +16,11 @@ import org.apache.http.params.HttpParams; import org.junit.Test; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; import java.io.IOException; +import java.io.ObjectInputStream; +import java.io.ObjectOutputStream; public class StreamTest { @Test @@ -53,6 +57,31 @@ public void testWithNewStreamUrl() throws Exception { assertThat(s1.expires, not(is(s2.expires))); } + @Test + public void shouldBeSerializable() throws Exception { + Stream s1 = new Stream( + "http://api.soundcloud.com", + "http://ak-media.soundcloud.com/Nbhil06qjDaP.128.mp3?AWSAccessKeyId=AKIAJBHW5FB4ERKUQUOQ&Expires=1319537336&Signature=tzk9EAm3bcjpMJ0cukHPdVx2ml4%3D&__gda__=1319537336_9354e7fea41da4f7a87e78db9a4ed582", + parse("s3-headers.txt")); + + ByteArrayOutputStream bos = new ByteArrayOutputStream(); + ObjectOutputStream oos = new ObjectOutputStream(bos); + + oos.writeObject(s1); + oos.close(); + + ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(bos.toByteArray())); + Stream s2 = (Stream) ois.readObject(); + + assertThat(s1.eTag, equalTo(s2.eTag)); + assertThat(s1.bitRate, equalTo(s2.bitRate)); + assertThat(s1.duration, equalTo(s2.duration)); + assertThat(s1.contentLength, equalTo(s2.contentLength)); + assertThat(s1.lastModified, equalTo(s2.lastModified)); + assertThat(s1.streamUrl, equalTo(s2.streamUrl)); + assertThat(s1.expires, equalTo(s2.expires)); + } + private HttpResponse parse(final String resource) throws IOException, HttpException { final HttpParams params = new BasicHttpParams(); HttpResponseParser parser = new HttpResponseParser(new AbstractSessionInputBuffer() { From 3886a5af2b3f5f09bf4667fbed9486bc2fc8d2da Mon Sep 17 00:00:00 2001 From: Jan Berkel Date: Fri, 4 Nov 2011 15:06:19 +0100 Subject: [PATCH 028/156] Facebook login example --- CHANGES.md | 1 + README.md | 2 + build.gradle | 1 + .../api/examples/FacebookConnect.java | 123 ++++++++++++++++++ .../api/CloudAPIIntegrationTest.java | 1 + 5 files changed, 128 insertions(+) create mode 100644 src/examples/java/com/soundcloud/api/examples/FacebookConnect.java diff --git a/CHANGES.md b/CHANGES.md index 49f1e67..9f5ef88 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -10,6 +10,7 @@ * Added PostResource example * Added support for HEAD requests * Added stream resolving + * Added Facebook login example ## 1.0.1 2011-07-04 diff --git a/README.md b/README.md index 7ba380d..3cbca33 100644 --- a/README.md +++ b/README.md @@ -120,6 +120,7 @@ The wrapper ships with a few examples in `src/examples/java`: * [PutResource][] performs a PUT request to update a resource and prints the JSON result * [UploadFile][] uploads a file to SoundCloud. + * [FacebookConnect][] obtain an access token via Facebook login You can use gradle tasks to compile and run these examples with one command. If you don't want to use gradle there is also a precompiled jar with all @@ -257,6 +258,7 @@ See LICENSE for details. [PutResource]: https://github.com/soundcloud/java-api-wrapper/blob/master/src/examples/java/com/soundcloud/api/examples/PutResource.java [PostResource]: https://github.com/soundcloud/java-api-wrapper/blob/master/src/examples/java/com/soundcloud/api/examples/PostResource.java [UploadFile]: https://github.com/soundcloud/java-api-wrapper/blob/master/src/examples/java/com/soundcloud/api/examples/UploadFile.java +[FacebookConnect]: https://github.com/soundcloud/java-api-wrapper/blob/master/src/examples/java/com/soundcloud/api/examples/FacebookConnect.java [SoundCloud Android]: https://market.android.com/details?id=com.soundcloud.android [register-app]: http://soundcloud.com/you/apps/new [Apache Maven]: http://maven.apache.org/ diff --git a/build.gradle b/build.gradle index 28c82ce..9338325 100644 --- a/build.gradle +++ b/build.gradle @@ -75,6 +75,7 @@ def example(name, mainClass, arguments) { } } example('createWrapper', 'CreateWrapper', { [client_id, client_secret, login, password, env] }) +example('facebookLogin', 'FacebookLogin', { [] }) example('getResource', 'GetResource', { resource }) example('putResource', 'PutResource', { [resource, content, contentType] }) example('postResource', 'PostResource', { [resource, content, contentType] }) diff --git a/src/examples/java/com/soundcloud/api/examples/FacebookConnect.java b/src/examples/java/com/soundcloud/api/examples/FacebookConnect.java new file mode 100644 index 0000000..bfa723c --- /dev/null +++ b/src/examples/java/com/soundcloud/api/examples/FacebookConnect.java @@ -0,0 +1,123 @@ +package com.soundcloud.api.examples; + +import com.soundcloud.api.ApiWrapper; +import com.soundcloud.api.CloudAPI; +import com.soundcloud.api.Endpoints; +import com.soundcloud.api.Env; +import com.soundcloud.api.Token; + +import java.awt.*; +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.PrintStream; +import java.io.UnsupportedEncodingException; +import java.net.ServerSocket; +import java.net.Socket; +import java.net.URI; +import java.net.URLDecoder; +import java.util.HashMap; +import java.util.Map; + + +/** + * This example shows how to get an API token by logging in w/ Facebook. + */ +public class FacebookConnect { + // http://sandbox-soundcloud.com/you/apps/java-api-wrapper-test-app + // user: api-testing + + //https://skitch.com/jberkel/ggb18/edit-java-api-wrapper-test-app-on-soundcloud-create-record-and-share-your-sounds-for-free + static final String CLIENT_ID = "yH1Jv2C5fhIbZfGTpKtujQ"; + static final String CLIENT_SECRET = "C6o8jc517b6PIw0RKtcfQsbOK3BjGpxWFLg977UiguY"; + static final URI REDIRECT_URI = URI.create("http://localhost:8000"); + + public static void main(String[] args) throws IOException { + final ApiWrapper wrapper = new ApiWrapper( + CLIENT_ID, + CLIENT_SECRET, + REDIRECT_URI, + null /* token */, + Env.SANDBOX); + + + String url = wrapper.authorizationCodeUrl(Endpoints.FACEBOOK_CONNECT, Token.SCOPE_NON_EXPIRING).toString(); + if (Desktop.isDesktopSupported() && Desktop.getDesktop().isSupported(Desktop.Action.BROWSE)) { + Desktop.getDesktop().browse(URI.create(url)); + } else { + System.err.println("open \"" + url + "\" in a browser"); + } + startServer(wrapper); + } + + static void startServer(ApiWrapper wrapper) throws IOException { + ServerSocket socket = new ServerSocket(8000); + for (;;) { + final Socket client = socket.accept(); + try { + InputStream is = client.getInputStream(); + BufferedReader reader = new BufferedReader(new InputStreamReader(is), 8192); + PrintStream out = new PrintStream(client.getOutputStream()); + String line = reader.readLine(); + if (line == null) throw new IOException("client closed connection without a request."); + + final String[] request = line.split(" ", 3); + if (request.length != 3) throw new IOException("invalid request:" + line); + if (!"GET".equals(request[0])) throw new IOException("invalid method:" + line); + + Map params = parseParameters(request[1]); + + if (params.containsKey("error")) { + reply(out, "Error: " + params.get("error_description")); + } else if (params.containsKey("code")) { + try { + Token token = wrapper.authorizationCode(params.get("code")); + reply(out, "Got token: " + token); + } catch (CloudAPI.InvalidTokenException e) { + reply(out, e.getMessage()); + } + } else { + reply(out, "invalid request:"+request[1]); + } + break; + } catch (IOException e) { + e.printStackTrace(); + } finally { + try { + client.close(); + } catch (IOException ignored) { + } + } + } + } + + static void reply(PrintStream out, String text) { + System.out.println(text); + + out.println("HTTP/1.1 200 OK"); + out.println("Content-Type: text/plain"); + out.println(); + out.println(text); + out.flush(); + } + + static Map parseParameters(String request) { + Map params = new HashMap(); + if (request.contains("?")) { + String query = request.substring(Math.min(request.length(), request.indexOf("?") + 1), + request.length()); + for (String s : query.split("&")) { + String[] kv = s.split("=", 2); + if (kv != null && kv.length == 2) { + try { + params.put(URLDecoder.decode(kv[0], "UTF-8"), + URLDecoder.decode(kv[1], "UTF-8")); + } catch (UnsupportedEncodingException ignored) { + } + } + } + } + return params; + } +} diff --git a/src/test/java/com/soundcloud/api/CloudAPIIntegrationTest.java b/src/test/java/com/soundcloud/api/CloudAPIIntegrationTest.java index 5ed7747..a18065e 100644 --- a/src/test/java/com/soundcloud/api/CloudAPIIntegrationTest.java +++ b/src/test/java/com/soundcloud/api/CloudAPIIntegrationTest.java @@ -27,6 +27,7 @@ public class CloudAPIIntegrationTest implements Params.Track, Endpoints { // http://sandbox-soundcloud.com/you/apps/java-api-wrapper-test-app + // user: api-testing static final String CLIENT_ID = "yH1Jv2C5fhIbZfGTpKtujQ"; static final String CLIENT_SECRET = "C6o8jc517b6PIw0RKtcfQsbOK3BjGpxWFLg977UiguY"; From d082073fb394f0fb84cfbaae561cc00b1c59ea56 Mon Sep 17 00:00:00 2001 From: Jan Berkel Date: Fri, 4 Nov 2011 15:12:00 +0100 Subject: [PATCH 029/156] Added Android usage notes --- .../api/examples/FacebookConnect.java | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/src/examples/java/com/soundcloud/api/examples/FacebookConnect.java b/src/examples/java/com/soundcloud/api/examples/FacebookConnect.java index bfa723c..8ed2004 100644 --- a/src/examples/java/com/soundcloud/api/examples/FacebookConnect.java +++ b/src/examples/java/com/soundcloud/api/examples/FacebookConnect.java @@ -48,7 +48,26 @@ public static void main(String[] args) throws IOException { } else { System.err.println("open \"" + url + "\" in a browser"); } + + // start a web server to get the redirect information startServer(wrapper); + + + // note: on Android you would use a WebView instead an override 'shouldOverrideUrlLoading': + + /* + WebView webView = (WebView) findViewById(R.id.webview); + webView.setWebViewClient(new WebViewClient() { + @Override + public boolean shouldOverrideUrlLoading(final WebView view, String url) { + if (url.startsWith(REDIRECT_URI)) { + Uri result = Uri.parse(url); + String error = result.getQueryParameter("error"); + String code = result.getQueryParameter("code"); + } + } + }); + */ } static void startServer(ApiWrapper wrapper) throws IOException { From c39a1102f932ab044e0ca73df6c093db4d32587f Mon Sep 17 00:00:00 2001 From: Jan Berkel Date: Fri, 4 Nov 2011 15:15:20 +0100 Subject: [PATCH 030/156] Already have a URI --- .../java/com/soundcloud/api/examples/FacebookConnect.java | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/examples/java/com/soundcloud/api/examples/FacebookConnect.java b/src/examples/java/com/soundcloud/api/examples/FacebookConnect.java index 8ed2004..651e2da 100644 --- a/src/examples/java/com/soundcloud/api/examples/FacebookConnect.java +++ b/src/examples/java/com/soundcloud/api/examples/FacebookConnect.java @@ -41,10 +41,9 @@ public static void main(String[] args) throws IOException { null /* token */, Env.SANDBOX); - - String url = wrapper.authorizationCodeUrl(Endpoints.FACEBOOK_CONNECT, Token.SCOPE_NON_EXPIRING).toString(); + URI url = wrapper.authorizationCodeUrl(Endpoints.FACEBOOK_CONNECT, Token.SCOPE_NON_EXPIRING); if (Desktop.isDesktopSupported() && Desktop.getDesktop().isSupported(Desktop.Action.BROWSE)) { - Desktop.getDesktop().browse(URI.create(url)); + Desktop.getDesktop().browse(url); } else { System.err.println("open \"" + url + "\" in a browser"); } From 9178f8f23f983acca092d884f343ce80d2746bd9 Mon Sep 17 00:00:00 2001 From: Jan Berkel Date: Fri, 4 Nov 2011 15:23:29 +0100 Subject: [PATCH 031/156] Document example --- README.md | 4 +++ .../api/examples/FacebookConnect.java | 28 +++++++++++-------- 2 files changed, 21 insertions(+), 11 deletions(-) diff --git a/README.md b/README.md index 3cbca33..a2ab92c 100644 --- a/README.md +++ b/README.md @@ -85,6 +85,10 @@ URI uri = wrapper.authorizationCodeUrl(Endpoints.CONNECT, Token.SCOPE_NON_EXPIRI // open uri in browser / WebView etc. ``` +## Login via Facebook + +Please see [FacebookConnect][] for an example of this login flow. + ## Requirements The wrapper depends on [Apache HttpClient][] (including the [HttpMime][] diff --git a/src/examples/java/com/soundcloud/api/examples/FacebookConnect.java b/src/examples/java/com/soundcloud/api/examples/FacebookConnect.java index 651e2da..5fc4525 100644 --- a/src/examples/java/com/soundcloud/api/examples/FacebookConnect.java +++ b/src/examples/java/com/soundcloud/api/examples/FacebookConnect.java @@ -41,6 +41,7 @@ public static void main(String[] args) throws IOException { null /* token */, Env.SANDBOX); + // generate the URL the user needs to open in the browser URI url = wrapper.authorizationCodeUrl(Endpoints.FACEBOOK_CONNECT, Token.SCOPE_NON_EXPIRING); if (Desktop.isDesktopSupported() && Desktop.getDesktop().isSupported(Desktop.Action.BROWSE)) { Desktop.getDesktop().browse(url); @@ -51,21 +52,22 @@ public static void main(String[] args) throws IOException { // start a web server to get the redirect information startServer(wrapper); - // note: on Android you would use a WebView instead an override 'shouldOverrideUrlLoading': /* - WebView webView = (WebView) findViewById(R.id.webview); - webView.setWebViewClient(new WebViewClient() { - @Override - public boolean shouldOverrideUrlLoading(final WebView view, String url) { - if (url.startsWith(REDIRECT_URI)) { - Uri result = Uri.parse(url); - String error = result.getQueryParameter("error"); - String code = result.getQueryParameter("code"); + WebView webView = (WebView) findViewById(R.id.webview); + webView.setWebViewClient(new WebViewClient() { + @Override + public boolean shouldOverrideUrlLoading(final WebView view, String url) { + if (url.startsWith(REDIRECT_URI)) { + Uri result = Uri.parse(url); + String error = result.getQueryParameter("error"); + String code = result.getQueryParameter("code"); + } } - } - }); + }); + + webView.loadUrl(wrapper.authorizationCodeUrl(Endpoints.FACEBOOK_CONNECT, ...); */ } @@ -87,8 +89,11 @@ static void startServer(ApiWrapper wrapper) throws IOException { Map params = parseParameters(request[1]); if (params.containsKey("error")) { + // error logging in, redirect mismatch etc. + reply(out, "Error: " + params.get("error_description")); } else if (params.containsKey("code")) { + // we got a code back, try to exchange it for a token try { Token token = wrapper.authorizationCode(params.get("code")); reply(out, "Got token: " + token); @@ -96,6 +101,7 @@ static void startServer(ApiWrapper wrapper) throws IOException { reply(out, e.getMessage()); } } else { + // unexpected redirect reply(out, "invalid request:"+request[1]); } break; From ebd3ce8b1dd4ac4339b5f9a7607a640e2968103a Mon Sep 17 00:00:00 2001 From: Jan Berkel Date: Wed, 9 Nov 2011 13:34:32 +0100 Subject: [PATCH 032/156] Make getUserAgent() public --- .../java/com/soundcloud/api/ApiWrapper.java | 19 ++++++++++--------- .../java/com/soundcloud/api/CloudAPI.java | 2 +- 2 files changed, 11 insertions(+), 10 deletions(-) diff --git a/src/main/java/com/soundcloud/api/ApiWrapper.java b/src/main/java/com/soundcloud/api/ApiWrapper.java index 5929e8e..6525b81 100644 --- a/src/main/java/com/soundcloud/api/ApiWrapper.java +++ b/src/main/java/com/soundcloud/api/ApiWrapper.java @@ -238,6 +238,15 @@ public URI getURI(Request request, boolean api, boolean secure) { return URI.create((api ? env.getResourceHost(secure) : env.getAuthResourceHost(secure)).toURI()).resolve(request.toUrl()); } + /** + * User-Agent to identify ourselves with - defaults to USER_AGENT + * @return the agent to use + * @see CloudAPI#USER_AGENT + */ + public String getUserAgent() { + return USER_AGENT; + } + /** * Request an OAuth2 token from SoundCloud * @param request the token request @@ -312,14 +321,6 @@ protected SSLSocketFactory getSSLSocketFactory() { return SSLSocketFactory.getSocketFactory(); } - /** - * User-Agent to identify ourselves with - defaults to USER_AGENT - * @return the agent to use - * @see CloudAPI#USER_AGENT - */ - protected String getUserAgent() { - return USER_AGENT; - } /** @return The HttpClient instance used to make the calls */ public HttpClient getHttpClient() { @@ -413,7 +414,7 @@ public long resolve(String url) throws IOException { } @Override - public Stream resolveStreamUrl(String url) throws IOException { + public Stream resolveStreamUrl(final String url) throws IOException { HttpResponse resp = head(Request.to(url)); if (resp.getStatusLine().getStatusCode() == HttpStatus.SC_MOVED_TEMPORARILY) { Header location = resp.getFirstHeader("Location"); diff --git a/src/main/java/com/soundcloud/api/CloudAPI.java b/src/main/java/com/soundcloud/api/CloudAPI.java index fe4a4ca..1944213 100644 --- a/src/main/java/com/soundcloud/api/CloudAPI.java +++ b/src/main/java/com/soundcloud/api/CloudAPI.java @@ -23,7 +23,7 @@ public interface CloudAPI { // other constants String REALM = "SoundCloud"; String OAUTH_SCHEME = "oauth"; - String VERSION = "1.0.1"; + String VERSION = "1.1-SNAPSHOT"; String USER_AGENT = "SoundCloud Java Wrapper ("+VERSION+")"; /** From ce6b919abed3ddb7a207c8947ceb3ac1ab0683b1 Mon Sep 17 00:00:00 2001 From: Jan Berkel Date: Wed, 9 Nov 2011 14:44:49 +0100 Subject: [PATCH 033/156] Don't need 3 requests to resolve stream urls --- src/main/java/com/soundcloud/api/ApiWrapper.java | 15 ++++----------- .../java/com/soundcloud/api/ApiWrapperTest.java | 6 +++--- 2 files changed, 7 insertions(+), 14 deletions(-) diff --git a/src/main/java/com/soundcloud/api/ApiWrapper.java b/src/main/java/com/soundcloud/api/ApiWrapper.java index 6525b81..2d8524c 100644 --- a/src/main/java/com/soundcloud/api/ApiWrapper.java +++ b/src/main/java/com/soundcloud/api/ApiWrapper.java @@ -415,21 +415,14 @@ public long resolve(String url) throws IOException { @Override public Stream resolveStreamUrl(final String url) throws IOException { - HttpResponse resp = head(Request.to(url)); + HttpResponse resp = get(Request.to(url)); if (resp.getStatusLine().getStatusCode() == HttpStatus.SC_MOVED_TEMPORARILY) { Header location = resp.getFirstHeader("Location"); if (location != null && location.getValue() != null) { - final String headRedirect = location.getValue(); - resp = getHttpClient().execute(new HttpHead(headRedirect)); + final String redirect = location.getValue(); + resp = getHttpClient().execute(new HttpHead(redirect)); if (resp.getStatusLine().getStatusCode() == HttpStatus.SC_OK) { - Stream stream = new Stream(url, headRedirect, resp); - // need to do another GET request to have a URL ready for client usage - resp = get(Request.to(url)); - if (resp.getStatusLine().getStatusCode() == HttpStatus.SC_MOVED_TEMPORARILY) { - return stream.withNewStreamUrl(resp.getFirstHeader("Location").getValue()); - } else { - throw new ResolverException("Unexpected response code", resp); - } + return new Stream(url, redirect, resp); } else { throw new ResolverException("Unexpected response code", resp); } diff --git a/src/test/java/com/soundcloud/api/ApiWrapperTest.java b/src/test/java/com/soundcloud/api/ApiWrapperTest.java index 64fa782..9e55a08 100644 --- a/src/test/java/com/soundcloud/api/ApiWrapperTest.java +++ b/src/test/java/com/soundcloud/api/ApiWrapperTest.java @@ -250,10 +250,10 @@ public boolean matches(HttpRequest request) { assertThat(api.resolve("http://soundcloud.com/crazybob"), is(1000L)); } - @Test - public void resolveShouldReturnNegativeOneWhenInvalid() throws Exception { + @Test(expected = CloudAPI.ResolverException.class) + public void resolveShouldRaiseResolverExceptionWhenInvalid() throws Exception { layer.addPendingHttpResponse(404, "Not found"); - assertThat(api.resolve("http://soundcloud.com/nonexisto"), equalTo(-1L)); + api.resolve("http://soundcloud.com/nonexisto"); } @Test From c609d59d83200aac1bb08373a8d37280fe8c5e50 Mon Sep 17 00:00:00 2001 From: Jan Berkel Date: Wed, 9 Nov 2011 14:49:45 +0100 Subject: [PATCH 034/156] Prepare for release --- CHANGES.md | 2 +- build.gradle | 2 +- src/main/java/com/soundcloud/api/CloudAPI.java | 2 +- src/main/java/com/soundcloud/api/package-info.java | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 9f5ef88..918cc15 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,4 +1,4 @@ -## 1.1.0-SNAPSHOT 2011-08-xx +## 1.1.0 2011-11-09 * Support httpmime-4.1.x (GH-2) * Support for conditional GETs diff --git a/build.gradle b/build.gradle index 9338325..8545426 100644 --- a/build.gradle +++ b/build.gradle @@ -4,7 +4,7 @@ apply plugin: 'eclipse' apply plugin: 'maven' sourceCompatibility = 1.6 -version = '1.1.0-SNAPSHOT' +version = '1.1.0' group = 'com.soundcloud' repositories { mavenCentral() } diff --git a/src/main/java/com/soundcloud/api/CloudAPI.java b/src/main/java/com/soundcloud/api/CloudAPI.java index 1944213..ea14965 100644 --- a/src/main/java/com/soundcloud/api/CloudAPI.java +++ b/src/main/java/com/soundcloud/api/CloudAPI.java @@ -23,7 +23,7 @@ public interface CloudAPI { // other constants String REALM = "SoundCloud"; String OAUTH_SCHEME = "oauth"; - String VERSION = "1.1-SNAPSHOT"; + String VERSION = "1.1.0"; String USER_AGENT = "SoundCloud Java Wrapper ("+VERSION+")"; /** diff --git a/src/main/java/com/soundcloud/api/package-info.java b/src/main/java/com/soundcloud/api/package-info.java index 0136bd8..f567440 100644 --- a/src/main/java/com/soundcloud/api/package-info.java +++ b/src/main/java/com/soundcloud/api/package-info.java @@ -7,6 +7,6 @@ * * @see com.soundcloud.api.ApiWrapper * @author Jan Berkel - * @version 1.0.1, 07/04/11 + * @version 1.1.0, 09/11/11 */ package com.soundcloud.api; From b51c9a2e908fe4b55b8c76ac7c491d3a5b5b19c2 Mon Sep 17 00:00:00 2001 From: Jan Berkel Date: Wed, 9 Nov 2011 15:06:09 +0100 Subject: [PATCH 035/156] [maven-release-plugin] prepare release 1.1.0 --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 6f0443e..d7545b8 100644 --- a/pom.xml +++ b/pom.xml @@ -3,7 +3,7 @@ 4.0.0 com.soundcloud java-api-wrapper - 1.1.0-SNAPSHOT + 1.1.0 SoundCloud Java API wrapper SoundCloud Java API wrapper (OAuth2 only), works on Android https://github.com/soundcloud/java-api-wrapper#readme From d446072899ffc1a16daa6d8d641bebc13cae937f Mon Sep 17 00:00:00 2001 From: Jan Berkel Date: Wed, 9 Nov 2011 15:06:16 +0100 Subject: [PATCH 036/156] [maven-release-plugin] prepare for next development iteration --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index d7545b8..dbfb593 100644 --- a/pom.xml +++ b/pom.xml @@ -3,7 +3,7 @@ 4.0.0 com.soundcloud java-api-wrapper - 1.1.0 + 1.1.1-SNAPSHOT SoundCloud Java API wrapper SoundCloud Java API wrapper (OAuth2 only), works on Android https://github.com/soundcloud/java-api-wrapper#readme From 487675c67220b5e3b2a00fbc7746b7f23eafffa8 Mon Sep 17 00:00:00 2001 From: Jan Berkel Date: Wed, 9 Nov 2011 15:11:01 +0100 Subject: [PATCH 037/156] new links --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index a2ab92c..8be212a 100644 --- a/README.md +++ b/README.md @@ -254,7 +254,7 @@ See LICENSE for details. [Apache HttpClient]: http://hc.apache.org/httpcomponents-client-ga/ [HttpMime]: http://hc.apache.org/httpcomponents-client-ga/httpmime [json-java]: http://json.org/java/ -[javadoc]: http://soundcloud.github.com/java-api-wrapper/javadoc/1.0.1/com/soundcloud/api/package-summary.html +[javadoc]: http://soundcloud.github.com/java-api-wrapper/javadoc/1.1.0/com/soundcloud/api/package-summary.html [soundcloudapi-java]: http://code.google.com/p/soundcloudapi-java/ [soundcloudapi-java-annouce]: http://blog.soundcloud.com/2010/01/08/java-wrapper/ [CreateWrapper]: https://github.com/soundcloud/java-api-wrapper/blob/master/src/examples/java/com/soundcloud/api/examples/CreateWrapper.java @@ -266,7 +266,7 @@ See LICENSE for details. [SoundCloud Android]: https://market.android.com/details?id=com.soundcloud.android [register-app]: http://soundcloud.com/you/apps/new [Apache Maven]: http://maven.apache.org/ -[jar-all]: https://github.com/downloads/soundcloud/java-api-wrapper/java-api-wrapper-1.0.1-all.jar +[jar-all]: https://github.com/downloads/soundcloud/java-api-wrapper/java-api-wrapper-1.1.0-all.jar [downloads]: https://github.com/soundcloud/java-api-wrapper/archives/master [snapshots]: https://oss.sonatype.org/content/repositories/snapshots/com/soundcloud/java-api-wrapper/ [releases]: https://oss.sonatype.org/content/repositories/releases/com/soundcloud/java-api-wrapper/ From c427fe17413bfc34426af6cee5d4afe161e2f0f4 Mon Sep 17 00:00:00 2001 From: Jan Berkel Date: Wed, 9 Nov 2011 15:11:19 +0100 Subject: [PATCH 038/156] release doc --- RELEASE.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/RELEASE.md b/RELEASE.md index 8955bf9..5621018 100644 --- a/RELEASE.md +++ b/RELEASE.md @@ -24,7 +24,7 @@ Regenerate pom.xml This doesn't work properly at the moment - use `gradle uploadArchive` and copy `build/poms/pom-default.xml` to `pom.xml`. -Releasing to Sonatype OSS (staging) +## Releasing to Sonatype OSS (staging) (make sure there are no uncommitted changes in the repo) $ mvn -Dresume=false release:prepare # tag repo, bump pom.xml (needs SNAPSHOT tag in pom) @@ -39,7 +39,7 @@ staging repository which can be used for testing. Once everything works you select "Release" to actually release it to the [release repo][]. The release repo is synced with [Maven Central][]. -Releasing snapshot versions +## Releasing snapshot versions This is for releasing developer version of the package and can be done anytime, just make sure `build.gradle` version contains a `-SNAPSHOT` suffix, then run: From d8d6f9ec193ec28b2152f9dda06c0f7e41fec7b8 Mon Sep 17 00:00:00 2001 From: Jan Berkel Date: Thu, 10 Nov 2011 14:34:57 +0100 Subject: [PATCH 039/156] Revert "Don't need 3 requests to resolve stream urls" This reverts commit ce6b919abed3ddb7a207c8947ceb3ac1ab0683b1. --- src/main/java/com/soundcloud/api/ApiWrapper.java | 15 +++++++++++---- .../java/com/soundcloud/api/ApiWrapperTest.java | 6 +++--- 2 files changed, 14 insertions(+), 7 deletions(-) diff --git a/src/main/java/com/soundcloud/api/ApiWrapper.java b/src/main/java/com/soundcloud/api/ApiWrapper.java index 2d8524c..6525b81 100644 --- a/src/main/java/com/soundcloud/api/ApiWrapper.java +++ b/src/main/java/com/soundcloud/api/ApiWrapper.java @@ -415,14 +415,21 @@ public long resolve(String url) throws IOException { @Override public Stream resolveStreamUrl(final String url) throws IOException { - HttpResponse resp = get(Request.to(url)); + HttpResponse resp = head(Request.to(url)); if (resp.getStatusLine().getStatusCode() == HttpStatus.SC_MOVED_TEMPORARILY) { Header location = resp.getFirstHeader("Location"); if (location != null && location.getValue() != null) { - final String redirect = location.getValue(); - resp = getHttpClient().execute(new HttpHead(redirect)); + final String headRedirect = location.getValue(); + resp = getHttpClient().execute(new HttpHead(headRedirect)); if (resp.getStatusLine().getStatusCode() == HttpStatus.SC_OK) { - return new Stream(url, redirect, resp); + Stream stream = new Stream(url, headRedirect, resp); + // need to do another GET request to have a URL ready for client usage + resp = get(Request.to(url)); + if (resp.getStatusLine().getStatusCode() == HttpStatus.SC_MOVED_TEMPORARILY) { + return stream.withNewStreamUrl(resp.getFirstHeader("Location").getValue()); + } else { + throw new ResolverException("Unexpected response code", resp); + } } else { throw new ResolverException("Unexpected response code", resp); } diff --git a/src/test/java/com/soundcloud/api/ApiWrapperTest.java b/src/test/java/com/soundcloud/api/ApiWrapperTest.java index 9e55a08..64fa782 100644 --- a/src/test/java/com/soundcloud/api/ApiWrapperTest.java +++ b/src/test/java/com/soundcloud/api/ApiWrapperTest.java @@ -250,10 +250,10 @@ public boolean matches(HttpRequest request) { assertThat(api.resolve("http://soundcloud.com/crazybob"), is(1000L)); } - @Test(expected = CloudAPI.ResolverException.class) - public void resolveShouldRaiseResolverExceptionWhenInvalid() throws Exception { + @Test + public void resolveShouldReturnNegativeOneWhenInvalid() throws Exception { layer.addPendingHttpResponse(404, "Not found"); - api.resolve("http://soundcloud.com/nonexisto"); + assertThat(api.resolve("http://soundcloud.com/nonexisto"), equalTo(-1L)); } @Test From 6c806d66e0dabd9eb6331372c27c23050622c7a2 Mon Sep 17 00:00:00 2001 From: Jan Berkel Date: Thu, 10 Nov 2011 14:42:03 +0100 Subject: [PATCH 040/156] build version --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index 8545426..9e4010f 100644 --- a/build.gradle +++ b/build.gradle @@ -4,7 +4,7 @@ apply plugin: 'eclipse' apply plugin: 'maven' sourceCompatibility = 1.6 -version = '1.1.0' +version = '1.1.1-SNAPSHOT' group = 'com.soundcloud' repositories { mavenCentral() } From 00d5a0c776cc827080fdb468054634f799bf8efa Mon Sep 17 00:00:00 2001 From: Jan Berkel Date: Fri, 11 Nov 2011 16:17:47 +0100 Subject: [PATCH 041/156] Support skipping of playcounts --- .../java/com/soundcloud/api/ApiWrapper.java | 9 ++++-- .../java/com/soundcloud/api/CloudAPI.java | 4 ++- .../java/com/soundcloud/api/Endpoints.java | 4 ++- src/main/java/com/soundcloud/api/Params.java | 6 ++-- .../api/CloudAPIIntegrationTest.java | 30 ++++++++++++++----- 5 files changed, 38 insertions(+), 15 deletions(-) diff --git a/src/main/java/com/soundcloud/api/ApiWrapper.java b/src/main/java/com/soundcloud/api/ApiWrapper.java index 6525b81..68d0332 100644 --- a/src/main/java/com/soundcloud/api/ApiWrapper.java +++ b/src/main/java/com/soundcloud/api/ApiWrapper.java @@ -414,7 +414,7 @@ public long resolve(String url) throws IOException { } @Override - public Stream resolveStreamUrl(final String url) throws IOException { + public Stream resolveStreamUrl(final String url, boolean skipLogging) throws IOException { HttpResponse resp = head(Request.to(url)); if (resp.getStatusLine().getStatusCode() == HttpStatus.SC_MOVED_TEMPORARILY) { Header location = resp.getFirstHeader("Location"); @@ -424,7 +424,12 @@ public Stream resolveStreamUrl(final String url) throws IOException { if (resp.getStatusLine().getStatusCode() == HttpStatus.SC_OK) { Stream stream = new Stream(url, headRedirect, resp); // need to do another GET request to have a URL ready for client usage - resp = get(Request.to(url)); + Request req = Request.to(url); + if (skipLogging) { + // skip logging + req.with("skip_logging", "1"); + } + resp = get(req); if (resp.getStatusLine().getStatusCode() == HttpStatus.SC_MOVED_TEMPORARILY) { return stream.withNewStreamUrl(resp.getFirstHeader("Location").getValue()); } else { diff --git a/src/main/java/com/soundcloud/api/CloudAPI.java b/src/main/java/com/soundcloud/api/CloudAPI.java index ea14965..b386381 100644 --- a/src/main/java/com/soundcloud/api/CloudAPI.java +++ b/src/main/java/com/soundcloud/api/CloudAPI.java @@ -190,11 +190,13 @@ public interface CloudAPI { * Resolve the given SoundCloud stream URI * * @param uri SoundCloud stream URI, e.g. https://api.soundcloud.com/tracks/25272620/stream + * @param skipLogging skip logging the play of this track (client needs + * {@link com.soundcloud.api.Token#SCOPE_PLAYCOUNT}) * @return the resolved stream * @throws IOException network errors * @throws com.soundcloud.api.CloudAPI.ResolverException resolver error (invalid status etc) */ - Stream resolveStreamUrl(String uri) throws IOException; + Stream resolveStreamUrl(String uri, boolean skipLogging) throws IOException; /** @return the current token */ Token getToken(); diff --git a/src/main/java/com/soundcloud/api/Endpoints.java b/src/main/java/com/soundcloud/api/Endpoints.java index 2a02ae6..68055d6 100644 --- a/src/main/java/com/soundcloud/api/Endpoints.java +++ b/src/main/java/com/soundcloud/api/Endpoints.java @@ -2,7 +2,7 @@ /** * Various SoundCloud API endpoints. - * See the API docs for the most + * See the API docs for the most * recent listing. */ @SuppressWarnings({"UnusedDeclaration"}) @@ -45,4 +45,6 @@ public interface Endpoints { String SEND_PASSWORD = "/passwords/reset-instructions"; String CONNECT = "/connect"; String FACEBOOK_CONNECT = "/connect/via/facebook"; + + String PLAYS = "/plays"; } diff --git a/src/main/java/com/soundcloud/api/Params.java b/src/main/java/com/soundcloud/api/Params.java index ad10d28..66b908b 100644 --- a/src/main/java/com/soundcloud/api/Params.java +++ b/src/main/java/com/soundcloud/api/Params.java @@ -5,7 +5,7 @@ */ public interface Params { /** - * Tracks + * see developers.soundcloud.com/docs/api/tracks */ @SuppressWarnings({"UnusedDeclaration"}) interface Track { @@ -42,7 +42,7 @@ interface Track { } /** - * Users + * see developers.soundcloud.com/docs/api/users */ @SuppressWarnings({"UnusedDeclaration"}) interface User { @@ -63,7 +63,7 @@ interface User { } /** - * Comments + * see developers.soundcloud.com/docs/api/comments */ @SuppressWarnings({"UnusedDeclaration"}) interface Comment { diff --git a/src/test/java/com/soundcloud/api/CloudAPIIntegrationTest.java b/src/test/java/com/soundcloud/api/CloudAPIIntegrationTest.java index a18065e..4d363cf 100644 --- a/src/test/java/com/soundcloud/api/CloudAPIIntegrationTest.java +++ b/src/test/java/com/soundcloud/api/CloudAPIIntegrationTest.java @@ -2,15 +2,11 @@ import static junit.framework.Assert.assertTrue; import static org.hamcrest.CoreMatchers.*; -import static org.junit.Assert.assertNotNull; -import static org.junit.Assert.assertThat; -import static org.junit.Assert.fail; +import static org.junit.Assert.*; -import junit.framework.Assert; import org.apache.http.Header; import org.apache.http.HttpResponse; import org.apache.http.client.methods.HttpGet; -import org.hamcrest.CoreMatchers; import org.json.JSONObject; import org.junit.Before; import org.junit.Ignore; @@ -141,7 +137,7 @@ public void shouldResolveUrls() throws Exception { public void shouldResolveStreamUrls() throws Exception { login(); - Stream resolved = api.resolveStreamUrl("https://api.sandbox-soundcloud.com/tracks/2100832/stream"); + Stream resolved = api.resolveStreamUrl("https://api.sandbox-soundcloud.com/tracks/2100832/stream", false); assertThat(resolved.url, equalTo("https://api.sandbox-soundcloud.com/tracks/2100832/stream")); assertThat(resolved.streamUrl, containsString("http://ak-media.soundcloud.com/")); @@ -150,11 +146,29 @@ public void shouldResolveStreamUrls() throws Exception { assertThat(resolved.eTag, equalTo("\"1298a3c38b12dc055ad0f7beb956bc56\"")); } + @Test @Ignore /* playcounts not deployed on sandbox */ + public void shouldResolveStreamUrlAndSkipPlaycountLogging() throws Exception { + // need the playcount scope for this to work + assertTrue(login(Token.SCOPE_PLAYCOUNT).scoped(Token.SCOPE_PLAYCOUNT)); + + int count = Http.getJSON(api.get(Request.to("/tracks/2100832"))).getInt("playback_count"); + api.resolveStreamUrl("https://api.sandbox-soundcloud.com/tracks/2100832/stream", false); + int count2 = Http.getJSON(api.get(Request.to("/tracks/2100832"))).getInt("playback_count"); + + assertTrue(String.format("%d !> %d", count2, count), count2 > count); + + // resolve again, this time skipping count + api.resolveStreamUrl("https://api.sandbox-soundcloud.com/tracks/2100832/stream", true); + + int count3 = Http.getJSON(api.get(Request.to("/tracks/2100832"))).getInt("playback_count"); + assertTrue(String.format("%d != %d", count3, count2), count3 == count2); + } + @Test public void shouldThrowResolverExceptionWhenStreamCannotBeResolved() throws Exception { login(); try { - Stream s = api.resolveStreamUrl("https://api.sandbox-soundcloud.com/tracks/999919191/stream"); + Stream s = api.resolveStreamUrl("https://api.sandbox-soundcloud.com/tracks/999919191/stream", false); fail("expected resolver exception, got: "+s); } catch (CloudAPI.ResolverException e) { // expected @@ -166,7 +180,7 @@ public void shouldThrowResolverExceptionWhenStreamCannotBeResolved() throws Exce public void shouldSupportRangeRequest() throws Exception { login(); - Stream resolved = api.resolveStreamUrl("https://api.sandbox-soundcloud.com/tracks/2100832/stream"); + Stream resolved = api.resolveStreamUrl("https://api.sandbox-soundcloud.com/tracks/2100832/stream", false); assertThat(resolved.contentLength, is(19643L)); HttpResponse resp = api From 6a660244888506272a14c48ae40722909e154aa3 Mon Sep 17 00:00:00 2001 From: Jan Berkel Date: Fri, 11 Nov 2011 16:22:22 +0100 Subject: [PATCH 042/156] Fix tests --- src/test/java/com/soundcloud/api/ApiWrapperTest.java | 4 ++-- src/test/java/com/soundcloud/api/CloudAPIIntegrationTest.java | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/test/java/com/soundcloud/api/ApiWrapperTest.java b/src/test/java/com/soundcloud/api/ApiWrapperTest.java index 64fa782..bf9c659 100644 --- a/src/test/java/com/soundcloud/api/ApiWrapperTest.java +++ b/src/test/java/com/soundcloud/api/ApiWrapperTest.java @@ -250,10 +250,10 @@ public boolean matches(HttpRequest request) { assertThat(api.resolve("http://soundcloud.com/crazybob"), is(1000L)); } - @Test + @Test(expected = CloudAPI.ResolverException.class) public void resolveShouldReturnNegativeOneWhenInvalid() throws Exception { layer.addPendingHttpResponse(404, "Not found"); - assertThat(api.resolve("http://soundcloud.com/nonexisto"), equalTo(-1L)); + api.resolve("http://soundcloud.com/nonexisto"); } @Test diff --git a/src/test/java/com/soundcloud/api/CloudAPIIntegrationTest.java b/src/test/java/com/soundcloud/api/CloudAPIIntegrationTest.java index 4d363cf..f6b683c 100644 --- a/src/test/java/com/soundcloud/api/CloudAPIIntegrationTest.java +++ b/src/test/java/com/soundcloud/api/CloudAPIIntegrationTest.java @@ -86,7 +86,7 @@ public void shouldUploadASimpleAudioFileBytes() throws Exception { } - @Test(expected = IOException.class) + @Test(expected = IOException.class) @Ignore public void shouldNotGetASignupTokenWhenInofficialApp() throws Exception { login(); api.clientCredentials(); From d6f6a24c4c6ad26d2d47363d758cfcee76cf4a58 Mon Sep 17 00:00:00 2001 From: Jan Berkel Date: Mon, 14 Nov 2011 13:06:42 +0100 Subject: [PATCH 043/156] Endpoints --- src/main/java/com/soundcloud/api/Endpoints.java | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/main/java/com/soundcloud/api/Endpoints.java b/src/main/java/com/soundcloud/api/Endpoints.java index 68055d6..496c0a8 100644 --- a/src/main/java/com/soundcloud/api/Endpoints.java +++ b/src/main/java/com/soundcloud/api/Endpoints.java @@ -13,6 +13,8 @@ public interface Endpoints { String TRACK_DETAILS = "/tracks/%d"; String TRACK_COMMENTS = "/tracks/%d/comments"; String TRACK_FAVORITERS = "/tracks/%d/favoriters"; + String TRACK_PLAYS = "/tracks/%d/plays"; + String TRACK_PERMISSIONS = "/tracks/%d/permissions"; String USERS = "/users"; String USER_DETAILS = "/users/%d"; @@ -46,5 +48,5 @@ public interface Endpoints { String CONNECT = "/connect"; String FACEBOOK_CONNECT = "/connect/via/facebook"; - String PLAYS = "/plays"; + } From 95fe26331e15a8330a8f278c94a5aaaf2472f89e Mon Sep 17 00:00:00 2001 From: Jan Berkel Date: Mon, 14 Nov 2011 15:44:09 +0100 Subject: [PATCH 044/156] Respect system proxy settings --- CHANGES.md | 4 ++++ src/main/java/com/soundcloud/api/ApiWrapper.java | 15 +++++++++++++++ .../soundcloud/api/CloudAPIIntegrationTest.java | 10 ++++++++++ 3 files changed, 29 insertions(+) diff --git a/CHANGES.md b/CHANGES.md index 918cc15..b543538 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,3 +1,7 @@ +## 1.1.1-SNAPSHOT tbd + + * Respect system proxy settings + ## 1.1.0 2011-11-09 * Support httpmime-4.1.x (GH-2) diff --git a/src/main/java/com/soundcloud/api/ApiWrapper.java b/src/main/java/com/soundcloud/api/ApiWrapper.java index 68d0332..6c12cf8 100644 --- a/src/main/java/com/soundcloud/api/ApiWrapper.java +++ b/src/main/java/com/soundcloud/api/ApiWrapper.java @@ -2,6 +2,7 @@ import org.apache.http.ConnectionReuseStrategy; import org.apache.http.Header; +import org.apache.http.HttpHost; import org.apache.http.HttpRequest; import org.apache.http.HttpResponse; import org.apache.http.HttpStatus; @@ -27,6 +28,7 @@ import org.apache.http.conn.params.ConnManagerParams; import org.apache.http.conn.params.ConnPerRoute; import org.apache.http.conn.params.ConnPerRouteBean; +import org.apache.http.conn.params.ConnRoutePNames; import org.apache.http.conn.routing.HttpRoute; import org.apache.http.conn.routing.HttpRoutePlanner; import org.apache.http.conn.scheme.PlainSocketFactory; @@ -304,6 +306,19 @@ public int getMaxForRoute(HttpRoute httpRoute) { } } }); + + // apply system proxy settings + final String proxyHost = System.getProperty("http.proxyHost"); + final String proxyPort = System.getProperty("http.proxyPort"); + if (proxyHost != null) { + int port = 80; + try { + port = Integer.parseInt(proxyPort); + } catch (NumberFormatException ignored) { + } + params.setParameter(ConnRoutePNames.DEFAULT_PROXY, + new HttpHost(proxyHost, port)); + } return params; } diff --git a/src/test/java/com/soundcloud/api/CloudAPIIntegrationTest.java b/src/test/java/com/soundcloud/api/CloudAPIIntegrationTest.java index f6b683c..70591e2 100644 --- a/src/test/java/com/soundcloud/api/CloudAPIIntegrationTest.java +++ b/src/test/java/com/soundcloud/api/CloudAPIIntegrationTest.java @@ -17,6 +17,7 @@ import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; +import java.net.UnknownHostException; import java.nio.ByteBuffer; import java.util.concurrent.BrokenBarrierException; import java.util.concurrent.CyclicBarrier; @@ -259,6 +260,15 @@ public void shouldSupportConditionalGets() throws Exception { assertThat(resp.getStatusLine().getStatusCode(), is(304) /* not-modified */); } + @Test(expected = UnknownHostException.class) + public void shouldRespectProxySettings() throws Exception { + System.setProperty("http.proxyHost", "http://doesnotexist.example.com"); + try { + login(); + } finally { + System.clearProperty("http.proxyHost"); + } + } @Test @Ignore public void shouldSupportConcurrentConnectionsToApiHost() throws Exception { From 849e4f6790a8149c021f33c81d18bf98e447c9bf Mon Sep 17 00:00:00 2001 From: Jan Berkel Date: Mon, 14 Nov 2011 21:09:17 +0100 Subject: [PATCH 045/156] Proxy support --- .../java/com/soundcloud/api/ApiWrapper.java | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/src/main/java/com/soundcloud/api/ApiWrapper.java b/src/main/java/com/soundcloud/api/ApiWrapper.java index 6c12cf8..fb0d2c2 100644 --- a/src/main/java/com/soundcloud/api/ApiWrapper.java +++ b/src/main/java/com/soundcloud/api/ApiWrapper.java @@ -306,7 +306,6 @@ public int getMaxForRoute(HttpRoute httpRoute) { } } }); - // apply system proxy settings final String proxyHost = System.getProperty("http.proxyHost"); final String proxyPort = System.getProperty("http.proxyPort"); @@ -316,12 +315,25 @@ public int getMaxForRoute(HttpRoute httpRoute) { port = Integer.parseInt(proxyPort); } catch (NumberFormatException ignored) { } - params.setParameter(ConnRoutePNames.DEFAULT_PROXY, - new HttpHost(proxyHost, port)); + params.setParameter(ConnRoutePNames.DEFAULT_PROXY, new HttpHost(proxyHost, port)); } return params; } + /** + * @param proxy the proxy to use for the wrapper, or null to clear the current one. + */ + public void setProxy(URI proxy) { + getHttpClient().getParams().setParameter( + ConnRoutePNames.DEFAULT_PROXY, + proxy == null ? null : new HttpHost(proxy.getHost(), proxy.getPort(), proxy.getScheme())); + } + + + public boolean isProxySet() { + return getHttpClient().getParams().getParameter(ConnRoutePNames.DEFAULT_PROXY) != null; + } + /** * @return SocketFactory used by the underlying HttpClient */ From 35338802a29caa188f80b7373ab621a6a943f5dc Mon Sep 17 00:00:00 2001 From: Jan Berkel Date: Fri, 18 Nov 2011 17:51:14 +0100 Subject: [PATCH 046/156] Support for extension grant types --- .../java/com/soundcloud/api/ApiWrapper.java | 65 +++++++++++------- .../java/com/soundcloud/api/CloudAPI.java | 66 +++++++------------ src/main/java/com/soundcloud/api/Request.java | 8 +++ src/main/java/com/soundcloud/api/Token.java | 31 +++++++-- .../com/soundcloud/api/ApiWrapperTest.java | 9 +++ .../api/CloudAPIIntegrationTest.java | 15 +++-- .../java/com/soundcloud/api/TokenTest.java | 17 +++++ 7 files changed, 130 insertions(+), 81 deletions(-) diff --git a/src/main/java/com/soundcloud/api/ApiWrapper.java b/src/main/java/com/soundcloud/api/ApiWrapper.java index fb0d2c2..0d0e8dc 100644 --- a/src/main/java/com/soundcloud/api/ApiWrapper.java +++ b/src/main/java/com/soundcloud/api/ApiWrapper.java @@ -128,63 +128,66 @@ public ApiWrapper(String clientId, this.env = env; } - @Override public Token login(String username, String password) throws IOException { - return login(username, password, null); - } - - @Override public Token login(String username, String password, String scope) throws IOException { + @Override public Token login(String username, String password, String... scopes) throws IOException { if (username == null || password == null) { throw new IllegalArgumentException("username or password is null"); } - final Request request = Request.to(Endpoints.TOKEN).with( + final Request request = addScope(Request.to(Endpoints.TOKEN).with( "grant_type", PASSWORD, "client_id", mClientId, "client_secret", mClientSecret, "username", username, - "password", password); - if (scope != null) request.add("scope", scope); + "password", password), scopes); mToken = requestToken(request); return mToken; } - @Override public Token authorizationCode(String code) throws IOException { - return authorizationCode(code, null); - } - @Override public Token authorizationCode(String code, String scope) throws IOException { + + @Override public Token authorizationCode(String code, String... scopes) throws IOException { if (code == null) { throw new IllegalArgumentException("username or password is null"); } - final Request request = Request.to(Endpoints.TOKEN).with( + final Request request = addScope(Request.to(Endpoints.TOKEN).with( "grant_type", AUTHORIZATION_CODE, "client_id", mClientId, "client_secret", mClientSecret, "redirect_uri", mRedirectUri, - "code", code); - if (scope != null) request.add("scope", scope); - + "code", code), scopes); mToken = requestToken(request); return mToken; } - @Override public Token clientCredentials() throws IOException { - return clientCredentials(Token.SCOPE_SIGNUP); - } - @Override public Token clientCredentials(String scope) throws IOException { - final Request req = Request.to(Endpoints.TOKEN).with( + @Override public Token clientCredentials(String... scopes) throws IOException { + final Request req = addScope(Request.to(Endpoints.TOKEN).with( "grant_type", CLIENT_CREDENTIALS, "client_id", mClientId, - "client_secret", mClientSecret); - if (scope != null) req.add("scope", scope); + "client_secret", mClientSecret), scopes); + final Token token = requestToken(req); - if (scope != null && !token.scoped(scope)) { - throw new InvalidTokenException(-1, "Could not obtain requested scope '"+scope+"' (got: '" + + if (scopes != null) { + for (String scope : scopes) { + if (!token.scoped(scope)) { + throw new InvalidTokenException(-1, "Could not obtain requested scope '"+scope+"' (got: '" + token.scope + "')"); + } + } } return token; } + @Override + public Token extensionGrantType(String grantType, String... scopes) throws IOException { + final Request req = addScope(Request.to(Endpoints.TOKEN).with( + "grant_type", grantType, + "client_id", mClientId, + "client_secret", mClientSecret), scopes); + + mToken = requestToken(req); + return mToken; + } + @Override public Token refreshToken() throws IOException { if (mToken == null || mToken.refresh == null) throw new IllegalStateException("no refresh token available"); mToken = requestToken(Request.to(Endpoints.TOKEN).with( @@ -542,6 +545,18 @@ public void setDefaultContentType(String contentType) { mDefaultContentType = contentType; } + /* package */ static Request addScope(Request request, String[] scopes) { + if (scopes != null && scopes.length > 0) { + StringBuilder scope = new StringBuilder(); + for (int i=0; i - * Resource Owner Password Credentials. - * - * @param username SoundCloud username - * @param password SoundCloud password - * @return a valid token - * @throws com.soundcloud.api.CloudAPI.InvalidTokenException invalid token - * @throws IOException In case of network/server errors - */ - Token login(String username, String password) throws IOException; /** * Request a token using @@ -44,13 +38,13 @@ public interface CloudAPI { * * @param username SoundCloud username * @param password SoundCloud password - * @param scope the desired scope + * @param scopes the desired scope(s), or empty for default scope * @return a valid token * @throws com.soundcloud.api.CloudAPI.InvalidTokenException * invalid token * @throws IOException In case of network/server errors */ - Token login(String username, String password, String scope) throws IOException; + Token login(String username, String password, String... scopes) throws IOException; /** @@ -58,23 +52,13 @@ public interface CloudAPI { * Authorization Code, requesting a default scope. * * @param code the authorization code + * @param scopes the desired scope(s), or empty for default scope * @return a valid token * @throws com.soundcloud.api.CloudAPI.InvalidTokenException invalid token * @throws IOException In case of network/server errors */ - Token authorizationCode(String code) throws IOException; + Token authorizationCode(String code, String... scopes) throws IOException; - /** - * Request a token using - * Authorization Code with a specified scope. - * - * @param code the authorization code - * @param scope the desired scope - * @return a valid token - * @throws com.soundcloud.api.CloudAPI.InvalidTokenException invalid token - * @throws IOException In case of network/server errors - */ - Token authorizationCode(String code, String scope) throws IOException; /** * Request a "signup" token using @@ -85,27 +69,23 @@ public interface CloudAPI { * Also note that not all apps are allowed to request this token type (the wrapper throws * InvalidTokenException in this case). * + * @param scopes the desired scope(s), or empty for default scope * @return a valid token * @throws IOException IO/Error * @throws com.soundcloud.api.CloudAPI.InvalidTokenException if requested scope is not available */ - Token clientCredentials() throws IOException; + Token clientCredentials(String... scopes) throws IOException; - /** - * Requests a token using - * Client Credentials. - * - * Note that this token is not set as the current token in the wrapper - it should only be used - * for one request (typically the signup / user creation request). - * Also note that not all apps are allowed to request for all scopes (the wrapper throws - * InvalidTokenException in this case). - * - * @param scope the requested scope - * @return a valid token - * @throws IOException IO/Error - * @throws com.soundcloud.api.CloudAPI.InvalidTokenException if requested scope is not available + + /** + * Request a token using an + * extension grant type. + * @param grantType + * @param scopes + * @return + * @throws IOException */ - Token clientCredentials(String scope) throws IOException; + Token extensionGrantType(String grantType, String... scopes) throws IOException; /** * Tries to refresh the currently used access token with the refresh token. @@ -223,7 +203,7 @@ public interface CloudAPI { * * @param options auth endpoint to use (leave out for default), requested scope (leave out for default) * @return the URI to open in a browser/WebView etc. - * @see CloudAPI#authorizationCode(String) + * @see CloudAPI#authorizationCode(String, String...) */ URI authorizationCodeUrl(String... options); diff --git a/src/main/java/com/soundcloud/api/Request.java b/src/main/java/com/soundcloud/api/Request.java index c1a6ba7..3ff0521 100644 --- a/src/main/java/com/soundcloud/api/Request.java +++ b/src/main/java/com/soundcloud/api/Request.java @@ -305,6 +305,14 @@ public Request ifNoneMatch(String etag) { return this; } + public Map getParams() { + Map params = new HashMap(); + for (NameValuePair p : mParams) { + params.put(p.getName(), p.getValue()); + } + return params; + } + /** * Builds a request with the given set of parameters and files. * @param method the type of request to use diff --git a/src/main/java/com/soundcloud/api/Token.java b/src/main/java/com/soundcloud/api/Token.java index 83139b6..834387d 100644 --- a/src/main/java/com/soundcloud/api/Token.java +++ b/src/main/java/com/soundcloud/api/Token.java @@ -6,7 +6,9 @@ import java.io.IOException; import java.io.Serializable; import java.util.Date; -import java.util.Set; +import java.util.HashMap; +import java.util.Iterator; +import java.util.Map; /** * Represents an OAuth2 access/refresh token pair. @@ -32,6 +34,8 @@ public class Token implements Serializable { public String access, refresh, scope; public long expiresIn; + public final Map customParameters = new HashMap(); + /** * Constructs a new token with the given sub-tokens * @param access A token used by the client to make authenticated requests on behalf of the resource owner. @@ -55,13 +59,21 @@ public Token(String access, String refresh, String scope) { */ public Token(JSONObject json) throws IOException { try { - access = json.getString(ACCESS_TOKEN); - if (json.has(REFRESH_TOKEN)) { - // refresh token won't be set if we don't expire - refresh = json.getString(REFRESH_TOKEN); - expiresIn = System.currentTimeMillis() + json.getLong(EXPIRES_IN) * 1000; + for (Iterator it = json.keys(); it.hasNext(); ) { + String key = it.next().toString(); + if (ACCESS_TOKEN.equals(key)) { + access = json.getString(ACCESS_TOKEN); + } else if (REFRESH_TOKEN.equals(key)) { + // refresh token won't be set if we don't expire + refresh = json.getString(REFRESH_TOKEN); + expiresIn = System.currentTimeMillis() + json.getLong(EXPIRES_IN) * 1000; + } else if (SCOPE.equals(key)) { + scope = json.getString(SCOPE); + } else { + // custom parameter + customParameters.put(key, json.getString(key)); + } } - scope = json.getString(SCOPE); } catch (JSONException e) { throw new IOException(e.getMessage()); } @@ -101,6 +113,11 @@ public boolean valid() { return access != null && (scoped(SCOPE_NON_EXPIRING) || refresh != null); } + /** indicates whether this token was issued after a signup */ + public String getSignup() { + return customParameters.get("soundcloud:user:signup"); + } + @Override public String toString() { return "Token{" + diff --git a/src/test/java/com/soundcloud/api/ApiWrapperTest.java b/src/test/java/com/soundcloud/api/ApiWrapperTest.java index bf9c659..340d9fc 100644 --- a/src/test/java/com/soundcloud/api/ApiWrapperTest.java +++ b/src/test/java/com/soundcloud/api/ApiWrapperTest.java @@ -418,4 +418,13 @@ public void shouldSerializeAndDeserializeWrapper() throws Exception { other.invalidateToken(); verify(listener).onTokenInvalid(old); } + + @Test + public void testAddScope() throws Exception { + assertThat(ApiWrapper.addScope(new Request(), new String[] { "foo", "bar"}).getParams().get("scope"), + equalTo("foo bar")); + + assertFalse(ApiWrapper.addScope(new Request(), new String[] {}).getParams().containsKey("scope")); + assertFalse(ApiWrapper.addScope(new Request(), null).getParams().containsKey("scope")); + } } diff --git a/src/test/java/com/soundcloud/api/CloudAPIIntegrationTest.java b/src/test/java/com/soundcloud/api/CloudAPIIntegrationTest.java index 70591e2..1cc7b27 100644 --- a/src/test/java/com/soundcloud/api/CloudAPIIntegrationTest.java +++ b/src/test/java/com/soundcloud/api/CloudAPIIntegrationTest.java @@ -48,12 +48,8 @@ public void setUp() throws Exception { Env.SANDBOX); } - private Token login() throws IOException { - return login(null); - } - - private Token login(String scope) throws IOException { - return api.login("api-testing", "testing", scope); + private Token login(String... scopes) throws IOException { + return api.login("api-testing", "testing", scopes); } @Test @@ -93,6 +89,13 @@ public void shouldNotGetASignupTokenWhenInofficialApp() throws Exception { api.clientCredentials(); } + @Test(expected = CloudAPI.InvalidTokenException.class) + public void shouldGetATokenUsingExtensionGrantTypes() throws Exception { + // TODO + String fbToken = "fbToken"; + Token token = api.extensionGrantType(CloudAPI.FACEBOOK_GRANT_TYPE +fbToken); + } + @Test public void shouldReturn401WithInvalidToken() throws Exception { login(); diff --git a/src/test/java/com/soundcloud/api/TokenTest.java b/src/test/java/com/soundcloud/api/TokenTest.java index 1c2a34e..193c2cd 100644 --- a/src/test/java/com/soundcloud/api/TokenTest.java +++ b/src/test/java/com/soundcloud/api/TokenTest.java @@ -81,4 +81,21 @@ public void shouldParseJsonResponse() throws Exception { assertThat(t.refresh, equalTo("5678")); assertNotNull(t.getExpiresIn()); } + + @Test + public void shouldParseJsonWithCustomParameters() throws Exception { + Token t = new Token(new JSONObject("{\n" + + " \"access_token\": \"1234\",\n" + + " \"refresh_token\": \"5678\",\n" + + " \"expires_in\": 3600,\n" + + " \"scope\": \"*\",\n" + + " \"custom1\": \"foo\",\n" + + " \"soundcloud:user:signup\": \"baz\",\n" + + " \"custom2\": 23\n" + + "}")); + + assertThat(t.customParameters.get("custom1"), equalTo("foo")); + assertThat(t.customParameters.get("custom2"), equalTo("23")); + assertThat(t.getSignup(), equalTo("baz")); + } } From 583f119edd3d25d937207a7f580314fd61835914 Mon Sep 17 00:00:00 2001 From: Jan Berkel Date: Wed, 7 Dec 2011 14:27:01 +0100 Subject: [PATCH 047/156] devices endpoint --- src/main/java/com/soundcloud/api/Endpoints.java | 1 + 1 file changed, 1 insertion(+) diff --git a/src/main/java/com/soundcloud/api/Endpoints.java b/src/main/java/com/soundcloud/api/Endpoints.java index 496c0a8..cad051f 100644 --- a/src/main/java/com/soundcloud/api/Endpoints.java +++ b/src/main/java/com/soundcloud/api/Endpoints.java @@ -39,6 +39,7 @@ public interface Endpoints { String MY_FOLLOWING = "/me/followings/%d"; String MY_CONFIRMATION = "/me/email-confirmations"; String MY_FRIENDS = "/me/connections/friends"; + String MY_DEVICES = "/me/devices"; String SUGGESTED_USERS = "/users/suggested"; From a31e6a555aa41dea0b804db37fc9cef0bc6925e8 Mon Sep 17 00:00:00 2001 From: Jan Berkel Date: Wed, 7 Dec 2011 14:27:11 +0100 Subject: [PATCH 048/156] Prefix paths with "/" --- src/main/java/com/soundcloud/api/Request.java | 10 ++++++++- .../api/CloudAPIIntegrationTest.java | 21 ++++++++++++++----- .../java/com/soundcloud/api/RequestTest.java | 6 ++++++ 3 files changed, 31 insertions(+), 6 deletions(-) diff --git a/src/main/java/com/soundcloud/api/Request.java b/src/main/java/com/soundcloud/api/Request.java index 3ff0521..d35e299 100644 --- a/src/main/java/com/soundcloud/api/Request.java +++ b/src/main/java/com/soundcloud/api/Request.java @@ -64,7 +64,15 @@ public Request() {} * @param resource the base resource */ public Request(String resource) { - if (resource != null && resource.contains("?")) { + if (resource == null) throw new IllegalArgumentException("resource is null"); + + // make sure paths start with a slash + if (!(resource.startsWith("http:") || resource.startsWith("https:")) + && !resource.startsWith("/")) { + resource = "/"+resource; + } + + if (resource.contains("?")) { String query = resource.substring(Math.min(resource.length(), resource.indexOf("?")+1), resource.length()); for (String s : query.split("&")) { diff --git a/src/test/java/com/soundcloud/api/CloudAPIIntegrationTest.java b/src/test/java/com/soundcloud/api/CloudAPIIntegrationTest.java index 1cc7b27..d9e9eac 100644 --- a/src/test/java/com/soundcloud/api/CloudAPIIntegrationTest.java +++ b/src/test/java/com/soundcloud/api/CloudAPIIntegrationTest.java @@ -1,5 +1,6 @@ package com.soundcloud.api; +import static junit.framework.Assert.assertNotNull; import static junit.framework.Assert.assertTrue; import static org.hamcrest.CoreMatchers.*; import static org.junit.Assert.*; @@ -62,6 +63,9 @@ public void shouldUploadASimpleAudioFile() throws Exception { int status = resp.getStatusLine().getStatusCode(); assertThat(status, is(201)); + + Header location = resp.getFirstHeader("Location"); + assertNotNull(location); } @Test @@ -104,6 +108,14 @@ public void shouldReturn401WithInvalidToken() throws Exception { assertThat(resp.getStatusLine().getStatusCode(), is(401)); } + + @Test + public void shouldWorkWithRelativeUrls() throws Exception { + login(); + HttpResponse resp = api.get(Request.to("me")); + assertThat(resp.getStatusLine().getStatusCode(), is(200)); + } + @Test public void shouldRefreshAutomaticallyWhenTokenExpired() throws Exception { login(); @@ -140,14 +152,13 @@ public void shouldResolveUrls() throws Exception { @Test public void shouldResolveStreamUrls() throws Exception { login(); + Stream resolved = api.resolveStreamUrl("https://api.sandbox-soundcloud.com/tracks/2112881/stream", false); - Stream resolved = api.resolveStreamUrl("https://api.sandbox-soundcloud.com/tracks/2100832/stream", false); - - assertThat(resolved.url, equalTo("https://api.sandbox-soundcloud.com/tracks/2100832/stream")); + assertThat(resolved.url, equalTo("https://api.sandbox-soundcloud.com/tracks/2112881/stream")); assertThat(resolved.streamUrl, containsString("http://ak-media.soundcloud.com/")); assertTrue("expire should be in the future", resolved.expires > System.currentTimeMillis()); - assertThat(resolved.eTag, equalTo("\"1298a3c38b12dc055ad0f7beb956bc56\"")); + assertThat(resolved.eTag, equalTo("\"a1782cf9976c2bc26988929e956def26\"")); } @Test @Ignore /* playcounts not deployed on sandbox */ @@ -184,7 +195,7 @@ public void shouldThrowResolverExceptionWhenStreamCannotBeResolved() throws Exce public void shouldSupportRangeRequest() throws Exception { login(); - Stream resolved = api.resolveStreamUrl("https://api.sandbox-soundcloud.com/tracks/2100832/stream", false); + Stream resolved = api.resolveStreamUrl("https://api.sandbox-soundcloud.com/tracks/2112881/stream", false); assertThat(resolved.contentLength, is(19643L)); HttpResponse resp = api diff --git a/src/test/java/com/soundcloud/api/RequestTest.java b/src/test/java/com/soundcloud/api/RequestTest.java index 5a1e171..b71bc85 100644 --- a/src/test/java/com/soundcloud/api/RequestTest.java +++ b/src/test/java/com/soundcloud/api/RequestTest.java @@ -292,6 +292,12 @@ public void shouldHaveCopyConstructor() { assertThat(copy.getToken(),equalTo(orig.getToken())); } + + @Test(expected = IllegalArgumentException.class) + public void shouldNotAcceptNullStringInCtor() throws Exception { + new Request((String) null); + } + @Test public void shouldNotModifyOriginal() { Request orig = new Request("/foo").with("1", 2, "3",4); From 56b0ea3c830cfde43a5a949b06e166a065178ba7 Mon Sep 17 00:00:00 2001 From: Jan Berkel Date: Sun, 11 Dec 2011 23:53:56 +0100 Subject: [PATCH 049/156] It's sign-up and not signup --- src/main/java/com/soundcloud/api/Token.java | 2 +- src/test/java/com/soundcloud/api/TokenTest.java | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/java/com/soundcloud/api/Token.java b/src/main/java/com/soundcloud/api/Token.java index 834387d..f61dab8 100644 --- a/src/main/java/com/soundcloud/api/Token.java +++ b/src/main/java/com/soundcloud/api/Token.java @@ -115,7 +115,7 @@ public boolean valid() { /** indicates whether this token was issued after a signup */ public String getSignup() { - return customParameters.get("soundcloud:user:signup"); + return customParameters.get("soundcloud:user:sign-up"); } @Override diff --git a/src/test/java/com/soundcloud/api/TokenTest.java b/src/test/java/com/soundcloud/api/TokenTest.java index 193c2cd..f31b82f 100644 --- a/src/test/java/com/soundcloud/api/TokenTest.java +++ b/src/test/java/com/soundcloud/api/TokenTest.java @@ -90,7 +90,7 @@ public void shouldParseJsonWithCustomParameters() throws Exception { " \"expires_in\": 3600,\n" + " \"scope\": \"*\",\n" + " \"custom1\": \"foo\",\n" + - " \"soundcloud:user:signup\": \"baz\",\n" + + " \"soundcloud:user:sign-up\": \"baz\",\n" + " \"custom2\": 23\n" + "}")); From 72a7a271bc619ae1897ceee02ff304dfe26e2e3a Mon Sep 17 00:00:00 2001 From: Jan Berkel Date: Fri, 6 Jan 2012 18:14:49 +0100 Subject: [PATCH 050/156] Request#newResource --- src/main/java/com/soundcloud/api/Request.java | 18 ++++++++++++------ .../java/com/soundcloud/api/RequestTest.java | 9 +++++++++ 2 files changed, 21 insertions(+), 6 deletions(-) diff --git a/src/main/java/com/soundcloud/api/Request.java b/src/main/java/com/soundcloud/api/Request.java index d35e299..69b1c63 100644 --- a/src/main/java/com/soundcloud/api/Request.java +++ b/src/main/java/com/soundcloud/api/Request.java @@ -152,6 +152,16 @@ public Request with(Object... args) { return this; } + /** + * @param resource the new resource + * @return a new request with identical parameters except for the specified resource. + */ + public Request newResource(String resource) { + Request newRequest = new Request(this); + newRequest.mResource = resource; + return newRequest; + } + /** * The request should be made with a specific token. * @param token the token @@ -201,7 +211,7 @@ public Request withFile(String name, File file) { * Registers a file to be uploaded with a POST or PUT request. * @param name the name of the parameter * @param file the file to be submitted - * @param fileName the name of the uploaded file (overrides file parameter) + * @param fileName the name of the uploaded file (over rides file parameter) * @return this */ public Request withFile(String name, File file, String fileName) { @@ -243,7 +253,6 @@ public Request withFile(String name, byte[] data, String fileName) { return withFile(name, data, "upload"); } - /** * Registers binary data to be uploaded with a POST or PUT request. * @param name the name of the parameter @@ -427,7 +436,6 @@ public String toString() { return listener; } - /** * Updates about the amount of bytes already transferred. */ @@ -439,9 +447,7 @@ public static interface TransferProgressListener { public void transferred(long amount) throws IOException; } - - - static class ByteBufferBody extends AbstractContentBody { + /* package */ static class ByteBufferBody extends AbstractContentBody { private ByteBuffer mBuffer; public ByteBufferBody(ByteBuffer buffer) { diff --git a/src/test/java/com/soundcloud/api/RequestTest.java b/src/test/java/com/soundcloud/api/RequestTest.java index b71bc85..9ddc763 100644 --- a/src/test/java/com/soundcloud/api/RequestTest.java +++ b/src/test/java/com/soundcloud/api/RequestTest.java @@ -59,6 +59,15 @@ public void shouldSupportWith() throws Exception { assertThat(p.queryString(), equalTo("foo=100&baz=22.3&baz=66")); } + @Test + public void shouldCopyRequestWithNewResource() throws Exception { + Request p = new Request().with("foo", 100, "baz", 22.3f); + Request p2 = p.newResource("baz"); + assertThat(p, not(sameInstance(p2))); + assertThat(p2.toString(), + equalTo("Request{mResource='baz', params=[foo=100, baz=22.3], files=null, entity=null, mToken=null, listener=null}")); + } + @Test public void shouldImplementIterable() throws Exception { Request p = new Request().with("foo", 100, "baz", 22.3f); From c132524b86e45900006b56676e49aafa2262d502 Mon Sep 17 00:00:00 2001 From: "Michael S. Klishin" Date: Fri, 27 Jan 2012 04:46:53 +0400 Subject: [PATCH 051/156] Add .travis.yml --- .travis.yml | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 .travis.yml diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..6a2ce34 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,3 @@ +language: java +notifications: + email: false From bd3cf0260f574690cce7a62543be401a4df4e8e9 Mon Sep 17 00:00:00 2001 From: "Michael S. Klishin" Date: Fri, 27 Jan 2012 04:49:11 +0400 Subject: [PATCH 052/156] Prefer Gradle (the repo also has pom.xml) --- .travis.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.travis.yml b/.travis.yml index 6a2ce34..aae8918 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,3 +1,5 @@ language: java +install: gradle gradle assemble +script: gradle check notifications: email: false From a9268b14e8ca25f8b4d1a5e5d73e2d66b4779070 Mon Sep 17 00:00:00 2001 From: "Michael S. Klishin" Date: Fri, 27 Jan 2012 04:49:58 +0400 Subject: [PATCH 053/156] Oops --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index aae8918..f524cb4 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,5 +1,5 @@ language: java -install: gradle gradle assemble +install: gradle assemble script: gradle check notifications: email: false From 08206a408fc4de83d9c1c974a5b9f3b7dcbedaee Mon Sep 17 00:00:00 2001 From: "Michael S. Klishin" Date: Fri, 27 Jan 2012 04:55:16 +0400 Subject: [PATCH 054/156] Use gradle test per project README --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index f524cb4..c93d2de 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,5 +1,5 @@ language: java install: gradle assemble -script: gradle check +script: gradle test notifications: email: false From df477b25891c2cbcad578911fc80256d7dd4b79e Mon Sep 17 00:00:00 2001 From: Jan Berkel Date: Thu, 15 Mar 2012 18:17:16 +0100 Subject: [PATCH 055/156] Workaround for http://code.google.com/p/android/issues/detail?id=5255 --- .../java/com/soundcloud/api/ApiWrapper.java | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/src/main/java/com/soundcloud/api/ApiWrapper.java b/src/main/java/com/soundcloud/api/ApiWrapper.java index 0d0e8dc..294f1a9 100644 --- a/src/main/java/com/soundcloud/api/ApiWrapper.java +++ b/src/main/java/com/soundcloud/api/ApiWrapper.java @@ -20,6 +20,7 @@ import org.apache.http.client.methods.HttpPost; import org.apache.http.client.methods.HttpPut; import org.apache.http.client.methods.HttpRequestBase; +import org.apache.http.client.methods.HttpUriRequest; import org.apache.http.client.params.HttpClientParams; import org.apache.http.client.protocol.ClientContext; import org.apache.http.conn.ClientConnectionManager; @@ -516,8 +517,21 @@ public synchronized void setTokenListener(TokenListener listener) { * @return the HTTP response * @throws java.io.IOException network error etc. */ - public HttpResponse execute(HttpRequest req) throws IOException { - return getHttpClient().execute(env.sslResourceHost, addHeaders(req)); + public HttpResponse execute(HttpUriRequest req) throws IOException { + try { + return getHttpClient().execute(env.sslResourceHost, addHeaders(req)); + } catch (NullPointerException e) { + // this is a workaround for a broken httpclient version, + // cf. http://code.google.com/p/android/issues/detail?id=5255 + // NPE in DefaultRequestDirector.java:456 + if (!req.isAborted() && req.getParams().isParameterFalse("npe-retried")) { + req.getParams().setBooleanParameter("npe-retried", true); + return execute(req); + } else { + req.abort(); + throw new IOException(e); + } + } } protected HttpResponse execute(Request req, Class reqType) throws IOException { From 0d3a68263699df0904ccd38fabfac2521b6b28ad Mon Sep 17 00:00:00 2001 From: Jan Berkel Date: Thu, 15 Mar 2012 19:43:45 +0100 Subject: [PATCH 056/156] Workaround for http://code.google.com/p/android/issues/detail?id=2690 --- .../java/com/soundcloud/api/ApiWrapper.java | 29 +++++++++++++++---- src/main/java/com/soundcloud/api/Env.java | 1 - .../com/soundcloud/api/ApiWrapperTest.java | 17 +++++++++++ 3 files changed, 41 insertions(+), 6 deletions(-) diff --git a/src/main/java/com/soundcloud/api/ApiWrapper.java b/src/main/java/com/soundcloud/api/ApiWrapper.java index 294f1a9..6be0567 100644 --- a/src/main/java/com/soundcloud/api/ApiWrapper.java +++ b/src/main/java/com/soundcloud/api/ApiWrapper.java @@ -241,7 +241,8 @@ public Token extensionGrantType(String grantType, String... scopes) throws IOExc * @return a valid URI */ public URI getURI(Request request, boolean api, boolean secure) { - return URI.create((api ? env.getResourceHost(secure) : env.getAuthResourceHost(secure)).toURI()).resolve(request.toUrl()); + return URI.create((api ? env.getResourceHost(secure) : env.getAuthResourceHost(secure)).toURI()) + .resolve(request.toUrl()); } /** @@ -328,14 +329,32 @@ public int getMaxForRoute(HttpRoute httpRoute) { * @param proxy the proxy to use for the wrapper, or null to clear the current one. */ public void setProxy(URI proxy) { - getHttpClient().getParams().setParameter( - ConnRoutePNames.DEFAULT_PROXY, - proxy == null ? null : new HttpHost(proxy.getHost(), proxy.getPort(), proxy.getScheme())); + final HttpHost host; + if (proxy != null) { + Scheme scheme = getHttpClient() + .getConnectionManager() + .getSchemeRegistry() + .getScheme(proxy.getScheme()); + + host = new HttpHost(proxy.getHost(), scheme.resolvePort(proxy.getPort()), scheme.getName()); + } else { + host = null; + } + getHttpClient().getParams().setParameter(ConnRoutePNames.DEFAULT_PROXY, host); } + public URI getProxy() { + Object proxy = getHttpClient().getParams().getParameter(ConnRoutePNames.DEFAULT_PROXY); + if (proxy instanceof HttpHost) { + return URI.create(((HttpHost)proxy).toURI()); + } else { + return null; + } + } + public boolean isProxySet() { - return getHttpClient().getParams().getParameter(ConnRoutePNames.DEFAULT_PROXY) != null; + return getProxy() != null; } /** diff --git a/src/main/java/com/soundcloud/api/Env.java b/src/main/java/com/soundcloud/api/Env.java index 25d5b37..b46b938 100644 --- a/src/main/java/com/soundcloud/api/Env.java +++ b/src/main/java/com/soundcloud/api/Env.java @@ -6,7 +6,6 @@ * The environment to operate against. * Use SANDBOX for testing your app, and LIVE for production applications. */ -@SuppressWarnings({"UnusedDeclaration"}) public enum Env { /** The main production site, http://soundcloud.com */ LIVE("api.soundcloud.com", "soundcloud.com"), diff --git a/src/test/java/com/soundcloud/api/ApiWrapperTest.java b/src/test/java/com/soundcloud/api/ApiWrapperTest.java index 340d9fc..ac2b7aa 100644 --- a/src/test/java/com/soundcloud/api/ApiWrapperTest.java +++ b/src/test/java/com/soundcloud/api/ApiWrapperTest.java @@ -1,5 +1,7 @@ package com.soundcloud.api; +import static junit.framework.Assert.assertEquals; +import static junit.framework.Assert.assertFalse; import static junit.framework.Assert.fail; import static org.hamcrest.CoreMatchers.*; import static org.junit.Assert.*; @@ -427,4 +429,19 @@ public void testAddScope() throws Exception { assertFalse(ApiWrapper.addScope(new Request(), new String[] {}).getParams().containsKey("scope")); assertFalse(ApiWrapper.addScope(new Request(), null).getParams().containsKey("scope")); } + + @Test + public void shouldSetProxy() throws Exception { + assertFalse(api.isProxySet()); + URI proxy = URI.create("https://foo.com"); + assertEquals(proxy.getPort(), -1); + api.setProxy(proxy); + assertTrue(api.isProxySet()); + assertEquals("https://foo.com:443", api.getProxy().toString()); + + + api.setProxy(URI.create("https://foo.com:12345")); + assertEquals(URI.create("https://foo.com:12345"), api.getProxy()); + } + } From eb5eed1c6d8d62def68d2f9326bffcaeb6a16670 Mon Sep 17 00:00:00 2001 From: Jan Berkel Date: Thu, 15 Mar 2012 21:04:52 +0100 Subject: [PATCH 057/156] Tests for workarounds --- .../java/com/soundcloud/api/ApiWrapper.java | 15 +++++- .../com/soundcloud/api/ApiWrapperTest.java | 47 +++++++++++++++++-- 2 files changed, 58 insertions(+), 4 deletions(-) diff --git a/src/main/java/com/soundcloud/api/ApiWrapper.java b/src/main/java/com/soundcloud/api/ApiWrapper.java index 6be0567..3452ed1 100644 --- a/src/main/java/com/soundcloud/api/ApiWrapper.java +++ b/src/main/java/com/soundcloud/api/ApiWrapper.java @@ -548,8 +548,13 @@ public HttpResponse execute(HttpUriRequest req) throws IOException { return execute(req); } else { req.abort(); - throw new IOException(e); + throw new BrokenHttpClientException(e); } + } catch (IllegalArgumentException e) { + // more brokenness + // cf. http://code.google.com/p/android/issues/detail?id=2690 + req.abort(); + throw new BrokenHttpClientException(e); } } @@ -653,4 +658,12 @@ protected RequestDirector getRequestDirector(HttpRequestExecutor requestExec, httpProcessor, retryHandler, redirectHandler, targetAuthHandler, proxyAuthHandler, stateHandler, params); } + + public static class BrokenHttpClientException extends IOException { + private static final long serialVersionUID = -4764332412926419313L; + + BrokenHttpClientException(Throwable throwable) { + super(throwable); + } + } } diff --git a/src/test/java/com/soundcloud/api/ApiWrapperTest.java b/src/test/java/com/soundcloud/api/ApiWrapperTest.java index ac2b7aa..7429488 100644 --- a/src/test/java/com/soundcloud/api/ApiWrapperTest.java +++ b/src/test/java/com/soundcloud/api/ApiWrapperTest.java @@ -4,11 +4,13 @@ import static junit.framework.Assert.assertFalse; import static junit.framework.Assert.fail; import static org.hamcrest.CoreMatchers.*; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; import static org.junit.Assert.*; +import static org.junit.Assert.assertTrue; +import static org.mockito.Matchers.any; import static org.mockito.Matchers.anyString; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; +import static org.mockito.Mockito.*; import com.soundcloud.api.fakehttp.FakeHttpLayer; import com.soundcloud.api.fakehttp.RequestMatcher; @@ -20,10 +22,13 @@ import org.apache.http.HttpResponse; import org.apache.http.StatusLine; import org.apache.http.client.AuthenticationHandler; +import org.apache.http.client.HttpClient; import org.apache.http.client.HttpRequestRetryHandler; import org.apache.http.client.RedirectHandler; import org.apache.http.client.RequestDirector; import org.apache.http.client.UserTokenHandler; +import org.apache.http.client.methods.HttpGet; +import org.apache.http.client.methods.HttpUriRequest; import org.apache.http.conn.ClientConnectionManager; import org.apache.http.conn.ConnectionKeepAliveStrategy; import org.apache.http.conn.routing.HttpRoutePlanner; @@ -444,4 +449,40 @@ public void shouldSetProxy() throws Exception { assertEquals(URI.create("https://foo.com:12345"), api.getProxy()); } + @Test @SuppressWarnings("serial") + public void shouldHandleBrokenHttpClientNPE() throws Exception { + final HttpClient client = mock(HttpClient.class); + ApiWrapper broken = new ApiWrapper("invalid", "invalid", URI.create("redirect://me"), null, Env.SANDBOX) { + @Override + public HttpClient getHttpClient() { + return client; + } + }; + when(client.execute(any(HttpHost.class), any(HttpUriRequest.class))).thenThrow(new NullPointerException()); + try { + broken.execute(new HttpGet("/foo")); + fail("expected BrokenHttpClientException"); + } catch (ApiWrapper.BrokenHttpClientException expected) { + // make sure client retried request + verify(client, times(2)).execute(any(HttpHost.class), any(HttpUriRequest.class)); + } + } + + @Test @SuppressWarnings("serial") + public void shouldHandleBrokenHttpClientIAE() throws Exception { + final HttpClient client = mock(HttpClient.class); + ApiWrapper broken = new ApiWrapper("invalid", "invalid", URI.create("redirect://me"), null, Env.SANDBOX) { + @Override + public HttpClient getHttpClient() { + return client; + } + }; + when(client.execute(any(HttpHost.class), any(HttpUriRequest.class))).thenThrow(new IllegalArgumentException()); + try { + broken.execute(new HttpGet("/foo")); + fail("expected BrokenHttpClientException"); + } catch (ApiWrapper.BrokenHttpClientException expected) { + verify(client, times(1)).execute(any(HttpHost.class), any(HttpUriRequest.class)); + } + } } From f60700552b63e6690fafd4424ea2bc15e4332440 Mon Sep 17 00:00:00 2001 From: Jan Berkel Date: Mon, 19 Mar 2012 15:14:11 +0100 Subject: [PATCH 058/156] Added safeExecute(HttpHost, HttpUriRequest) --- .../java/com/soundcloud/api/ApiWrapper.java | 53 ++++++++++++------- .../java/com/soundcloud/api/CloudAPI.java | 22 ++++++++ .../com/soundcloud/api/ApiWrapperTest.java | 30 +++++++++++ 3 files changed, 86 insertions(+), 19 deletions(-) diff --git a/src/main/java/com/soundcloud/api/ApiWrapper.java b/src/main/java/com/soundcloud/api/ApiWrapper.java index 3452ed1..7970478 100644 --- a/src/main/java/com/soundcloud/api/ApiWrapper.java +++ b/src/main/java/com/soundcloud/api/ApiWrapper.java @@ -3,7 +3,6 @@ import org.apache.http.ConnectionReuseStrategy; import org.apache.http.Header; import org.apache.http.HttpHost; -import org.apache.http.HttpRequest; import org.apache.http.HttpResponse; import org.apache.http.HttpStatus; import org.apache.http.auth.AUTH; @@ -532,28 +531,36 @@ public synchronized void setTokenListener(TokenListener listener) { /** * Execute an API request, adds the necessary headers. - * @param req the HTTP request + * @param request the HTTP request * @return the HTTP response * @throws java.io.IOException network error etc. */ - public HttpResponse execute(HttpUriRequest req) throws IOException { + public HttpResponse execute(HttpUriRequest request) throws IOException { + return safeExecute(env.sslResourceHost, addHeaders(request)); + } + + public HttpResponse safeExecute(HttpHost target, HttpUriRequest request) throws IOException { + if (target == null) { + target = determineTarget(request); + } + try { - return getHttpClient().execute(env.sslResourceHost, addHeaders(req)); + return getHttpClient().execute(target, request); } catch (NullPointerException e) { // this is a workaround for a broken httpclient version, // cf. http://code.google.com/p/android/issues/detail?id=5255 // NPE in DefaultRequestDirector.java:456 - if (!req.isAborted() && req.getParams().isParameterFalse("npe-retried")) { - req.getParams().setBooleanParameter("npe-retried", true); - return execute(req); + if (!request.isAborted() && request.getParams().isParameterFalse("npe-retried")) { + request.getParams().setBooleanParameter("npe-retried", true); + return safeExecute(target, request); } else { - req.abort(); + request.abort(); throw new BrokenHttpClientException(e); } } catch (IllegalArgumentException e) { // more brokenness // cf. http://code.google.com/p/android/issues/detail?id=2690 - req.abort(); + request.abort(); throw new BrokenHttpClientException(e); } } @@ -563,6 +570,21 @@ protected HttpResponse execute(Request req, Class req return execute(req.buildRequest(reqType)); } + + protected HttpHost determineTarget(HttpUriRequest request) { + // A null target may be acceptable if there is a default target. + // Otherwise, the null target is detected in the director. + URI requestURI = request.getURI(); + if (requestURI.isAbsolute()) { + return new HttpHost( + requestURI.getHost(), + requestURI.getPort(), + requestURI.getScheme()); + } else { + return null; + } + } + /** * serialize the wrapper to a File * @param f target @@ -618,7 +640,7 @@ public static Header createOAuthHeader(Token token) { } /** Adds an OAuth2 header to a given request */ - protected HttpRequest addAuthHeader(HttpRequest request) { + protected HttpUriRequest addAuthHeader(HttpUriRequest request) { if (!request.containsHeader(AUTH.WWW_AUTH_RESP)) { request.addHeader(createOAuthHeader(getToken())); } @@ -626,7 +648,7 @@ protected HttpRequest addAuthHeader(HttpRequest request) { } /** Forces JSON */ - protected HttpRequest addAcceptHeader(HttpRequest request) { + protected HttpUriRequest addAcceptHeader(HttpUriRequest request) { if (!request.containsHeader("Accept")) { request.addHeader("Accept", getDefaultContentType()); } @@ -634,7 +656,7 @@ protected HttpRequest addAcceptHeader(HttpRequest request) { } /** Adds all required headers to the request */ - protected HttpRequest addHeaders(HttpRequest req) { + protected HttpUriRequest addHeaders(HttpUriRequest req) { return addAcceptHeader( addAuthHeader(req)); } @@ -659,11 +681,4 @@ protected RequestDirector getRequestDirector(HttpRequestExecutor requestExec, stateHandler, params); } - public static class BrokenHttpClientException extends IOException { - private static final long serialVersionUID = -4764332412926419313L; - - BrokenHttpClientException(Throwable throwable) { - super(throwable); - } - } } diff --git a/src/main/java/com/soundcloud/api/CloudAPI.java b/src/main/java/com/soundcloud/api/CloudAPI.java index 69dabdf..1f644be 100644 --- a/src/main/java/com/soundcloud/api/CloudAPI.java +++ b/src/main/java/com/soundcloud/api/CloudAPI.java @@ -1,7 +1,9 @@ package com.soundcloud.api; +import org.apache.http.HttpHost; import org.apache.http.HttpResponse; import org.apache.http.client.HttpClient; +import org.apache.http.client.methods.HttpUriRequest; import java.io.IOException; import java.net.URI; @@ -156,6 +158,17 @@ public interface CloudAPI { */ HttpClient getHttpClient(); + /** + * Generic execute method, with added workarounds for various HTTPClient bugs. + * + * @param target the target host (can be null) + * @param request the request + * @return the HTTP response + * @throws IOException network errors + * @throws BrokenHttpClientException in case of HTTPClient framework bugs + */ + HttpResponse safeExecute(HttpHost target, HttpUriRequest request) throws IOException; + /** * Resolve the given SoundCloud URI * @@ -271,4 +284,13 @@ public String getMessage() { return super.getMessage()+" "+(response != null ? response.getStatusLine() : ""); } } + + + class BrokenHttpClientException extends IOException { + private static final long serialVersionUID = -4764332412926419313L; + + BrokenHttpClientException(Throwable throwable) { + super(throwable); + } + } } diff --git a/src/test/java/com/soundcloud/api/ApiWrapperTest.java b/src/test/java/com/soundcloud/api/ApiWrapperTest.java index 7429488..ea54d4f 100644 --- a/src/test/java/com/soundcloud/api/ApiWrapperTest.java +++ b/src/test/java/com/soundcloud/api/ApiWrapperTest.java @@ -485,4 +485,34 @@ public HttpClient getHttpClient() { verify(client, times(1)).execute(any(HttpHost.class), any(HttpUriRequest.class)); } } + + @SuppressWarnings("serial") + @Test + public void shouldSafeExecute() throws Exception { + + final HttpClient client = mock(HttpClient.class); + ApiWrapper broken = new ApiWrapper("invalid", "invalid", URI.create("redirect://me"), null, Env.SANDBOX) { + @Override + public HttpClient getHttpClient() { + return client; + } + }; + when(client.execute(any(HttpHost.class), any(HttpUriRequest.class))).thenThrow(new IllegalArgumentException()); + try { + broken.safeExecute(null, new HttpGet("/foo")); + fail("expected BrokenHttpClientException"); + } catch (ApiWrapper.BrokenHttpClientException expected) { + verify(client, times(1)).execute(any(HttpHost.class), any(HttpUriRequest.class)); + } + + reset(client); + when(client.execute(any(HttpHost.class), any(HttpUriRequest.class))).thenThrow(new NullPointerException()); + try { + broken.execute(new HttpGet("/foo")); + fail("expected BrokenHttpClientException"); + } catch (ApiWrapper.BrokenHttpClientException expected) { + // make sure client retried request + verify(client, times(2)).execute(any(HttpHost.class), any(HttpUriRequest.class)); + } + } } From 2f1934ae605680c6b3cac8533f3d639bbb305bad Mon Sep 17 00:00:00 2001 From: Jan Berkel Date: Mon, 19 Mar 2012 15:33:57 +0100 Subject: [PATCH 059/156] Use safeExecute() in resolveStreamUrl() --- src/main/java/com/soundcloud/api/ApiWrapper.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/com/soundcloud/api/ApiWrapper.java b/src/main/java/com/soundcloud/api/ApiWrapper.java index 7970478..f6fa039 100644 --- a/src/main/java/com/soundcloud/api/ApiWrapper.java +++ b/src/main/java/com/soundcloud/api/ApiWrapper.java @@ -469,7 +469,7 @@ public Stream resolveStreamUrl(final String url, boolean skipLogging) throws IOE Header location = resp.getFirstHeader("Location"); if (location != null && location.getValue() != null) { final String headRedirect = location.getValue(); - resp = getHttpClient().execute(new HttpHead(headRedirect)); + resp = safeExecute(null, new HttpHead(headRedirect)); if (resp.getStatusLine().getStatusCode() == HttpStatus.SC_OK) { Stream stream = new Stream(url, headRedirect, resp); // need to do another GET request to have a URL ready for client usage From 13bf955698320402a494896640f396bd011d98ff Mon Sep 17 00:00:00 2001 From: Jan Berkel Date: Mon, 19 Mar 2012 15:39:01 +0100 Subject: [PATCH 060/156] safeExecute() --- src/main/java/com/soundcloud/api/ApiWrapper.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/com/soundcloud/api/ApiWrapper.java b/src/main/java/com/soundcloud/api/ApiWrapper.java index f6fa039..b605ced 100644 --- a/src/main/java/com/soundcloud/api/ApiWrapper.java +++ b/src/main/java/com/soundcloud/api/ApiWrapper.java @@ -261,7 +261,7 @@ public String getUserAgent() { * @throws com.soundcloud.api.CloudAPI.InvalidTokenException unauthorized */ protected Token requestToken(Request request) throws IOException { - HttpResponse response = getHttpClient().execute(env.sslResourceHost, request.buildRequest(HttpPost.class)); + HttpResponse response = safeExecute(env.sslResourceHost, request.buildRequest(HttpPost.class)); final int status = response.getStatusLine().getStatusCode(); if (status == HttpStatus.SC_OK) { From d5c3c79c2282001c16429cbee6b9de65e84f44a4 Mon Sep 17 00:00:00 2001 From: Jan Berkel Date: Tue, 24 Apr 2012 09:44:51 +0200 Subject: [PATCH 061/156] Fix build script for new version of gradle --- build.gradle | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/build.gradle b/build.gradle index 9e4010f..63f8916 100644 --- a/build.gradle +++ b/build.gradle @@ -11,7 +11,7 @@ repositories { mavenCentral() } sourceSets { examples { - compileClasspath = sourceSets.main.classes + sourceSets.main.runtimeClasspath + compileClasspath = sourceSets.main.output + sourceSets.main.runtimeClasspath } } @@ -59,7 +59,7 @@ uploadArchives { [compileJava, compileTestJava]*.options*.compilerArgs = ['-Xlint:all'] -httpDebug = ['org.apache.commons.logging.Log': 'org.apache.commons.logging.impl.SimpleLog', +ext.httpDebug = ['org.apache.commons.logging.Log': 'org.apache.commons.logging.impl.SimpleLog', 'org.apache.commons.logging.simplelog.showdatetime': 'true', 'org.apache.commons.logging.simplelog.log.org.apache.http': 'DEBUG', 'org.apache.commons.logging.simplelog.log.org.apache.http.wire': 'ERROR'] @@ -68,8 +68,8 @@ def example(name, mainClass, arguments) { task(name, dependsOn: ['compileJava', 'compileExamplesJava']) << { javaexec { main = 'com.soundcloud.api.examples.'+mainClass - classpath = sourceSets.main.runtimeClasspath + sourceSets.examples.classes - if (logger.debugEnabled) systemProperties httpDebug + classpath = sourceSets.main.runtimeClasspath + sourceSets.examples.output + if (logger.debugEnabled) systemProperties ext.httpDebug args arguments.call() } } @@ -101,17 +101,17 @@ task doc(type: Javadoc) { task jarAll(type: Jar) { description = "Build a jar file with all dependencies" - dependsOn configurations.runtime, sourceSets.main.classes, sourceSets.examples.classes + dependsOn configurations.runtime, sourceSets.main.output, sourceSets.examples.output archiveName = project.name + "-" + version +"-all.jar" - from { (configurations.runtime + sourceSets.main.classes + sourceSets.examples.classes ).collect { + from { (configurations.runtime + sourceSets.main.output + sourceSets.examples.output ).collect { it.isDirectory() ? it : zipTree(it) } } } def getAuth(repo_id) { - m2_settings = new File("${System.getProperty('user.home')}/.m2/settings.xml") - if (m2_settings.exists()) { - settings = new XmlSlurper().parse(m2_settings) + ext.m2_settings = new File("${System.getProperty('user.home')}/.m2/settings.xml") + if (ext.m2_settings.exists()) { + settings = new XmlSlurper().parse(ext.m2_settings) repo = settings.servers.server.find { it.id.text() == repo_id } if (repo != null) return [userName: repo.username.text(), password: repo.password.text()] } @@ -119,5 +119,5 @@ def getAuth(repo_id) { } task printDebug << { - println httpDebug.collect { "-D"+it.key+"="+it.value }.join(' ') + println ext.httpDebug.collect { "-D"+it.key+"="+it.value }.join(' ') } From 6a41b02e9245bda1009f9e7bfa799e2769e2ca41 Mon Sep 17 00:00:00 2001 From: Jan Berkel Date: Sat, 28 Apr 2012 14:31:47 +0200 Subject: [PATCH 062/156] Fix IOException ctor (Android SDK 9+ only) --- src/main/java/com/soundcloud/api/CloudAPI.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/main/java/com/soundcloud/api/CloudAPI.java b/src/main/java/com/soundcloud/api/CloudAPI.java index 1f644be..7abc412 100644 --- a/src/main/java/com/soundcloud/api/CloudAPI.java +++ b/src/main/java/com/soundcloud/api/CloudAPI.java @@ -290,7 +290,8 @@ class BrokenHttpClientException extends IOException { private static final long serialVersionUID = -4764332412926419313L; BrokenHttpClientException(Throwable throwable) { - super(throwable); + super(throwable.getMessage()); + initCause(throwable); } } } From 5b8c6fdae94fd862a92c36c56dea1b0213d75561 Mon Sep 17 00:00:00 2001 From: Jan Berkel Date: Mon, 30 Apr 2012 13:31:10 +0200 Subject: [PATCH 063/156] Changed another Android incompatible ctor --- src/main/java/com/soundcloud/api/CloudAPI.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/main/java/com/soundcloud/api/CloudAPI.java b/src/main/java/com/soundcloud/api/CloudAPI.java index 7abc412..a6e6aec 100644 --- a/src/main/java/com/soundcloud/api/CloudAPI.java +++ b/src/main/java/com/soundcloud/api/CloudAPI.java @@ -271,7 +271,8 @@ public ResolverException(String s, HttpResponse resp) { } public ResolverException(Throwable throwable, HttpResponse response) { - super(throwable); + super(throwable.getMessage()); + initCause(throwable); this.response = response; } From 8bb9fb0d8e46431225e3f4b62509120803b8fd05 Mon Sep 17 00:00:00 2001 From: Jan Berkel Date: Mon, 30 Apr 2012 15:34:35 +0200 Subject: [PATCH 064/156] Document changes --- CHANGES.md | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/CHANGES.md b/CHANGES.md index b543538..ef8a546 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,6 +1,9 @@ -## 1.1.1-SNAPSHOT tbd +## 1.1.1 2012-04-30 * Respect system proxy settings + * Added skip_logging parameter to resolveStreamUrl + * Added workarounds for some HTTPClient bugs + * Fixed some Android compatibility problems (IOException constructor) ## 1.1.0 2011-11-09 From 7a12d9f67087fcfe8d238579a57dfdf8116b6b6a Mon Sep 17 00:00:00 2001 From: Jan Berkel Date: Mon, 30 Apr 2012 15:57:13 +0200 Subject: [PATCH 065/156] NPE safe --- src/main/java/com/soundcloud/api/CloudAPI.java | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/main/java/com/soundcloud/api/CloudAPI.java b/src/main/java/com/soundcloud/api/CloudAPI.java index a6e6aec..1e1ee02 100644 --- a/src/main/java/com/soundcloud/api/CloudAPI.java +++ b/src/main/java/com/soundcloud/api/CloudAPI.java @@ -271,7 +271,7 @@ public ResolverException(String s, HttpResponse resp) { } public ResolverException(Throwable throwable, HttpResponse response) { - super(throwable.getMessage()); + super(throwable == null ? null : throwable.toString()); initCause(throwable); this.response = response; } @@ -286,12 +286,11 @@ public String getMessage() { } } - class BrokenHttpClientException extends IOException { private static final long serialVersionUID = -4764332412926419313L; BrokenHttpClientException(Throwable throwable) { - super(throwable.getMessage()); + super(throwable == null ? null : throwable.toString()); initCause(throwable); } } From aad9852cb849a967592cc2601ebbf6c29dc1b99b Mon Sep 17 00:00:00 2001 From: Jan Berkel Date: Wed, 2 May 2012 14:12:17 +0200 Subject: [PATCH 066/156] Fix deprecation warning --- build.gradle | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/build.gradle b/build.gradle index 63f8916..6ab021a 100644 --- a/build.gradle +++ b/build.gradle @@ -109,10 +109,10 @@ task jarAll(type: Jar) { } def getAuth(repo_id) { - ext.m2_settings = new File("${System.getProperty('user.home')}/.m2/settings.xml") - if (ext.m2_settings.exists()) { - settings = new XmlSlurper().parse(ext.m2_settings) - repo = settings.servers.server.find { it.id.text() == repo_id } + def m2_settings = new File("${System.getProperty('user.home')}/.m2/settings.xml") + if (m2_settings.exists()) { + def settings = new XmlSlurper().parse(m2_settings) + def repo = settings.servers.server.find { it.id.text() == repo_id } if (repo != null) return [userName: repo.username.text(), password: repo.password.text()] } [:] From c51e145f76a5e8b3ce51dd17652ed571085a48a4 Mon Sep 17 00:00:00 2001 From: Jan Berkel Date: Wed, 2 May 2012 14:19:46 +0200 Subject: [PATCH 067/156] Prepare release --- README.md | 4 ++-- build.gradle | 2 +- src/main/java/com/soundcloud/api/CloudAPI.java | 2 +- src/main/java/com/soundcloud/api/package-info.java | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 8be212a..475cfa6 100644 --- a/README.md +++ b/README.md @@ -254,7 +254,7 @@ See LICENSE for details. [Apache HttpClient]: http://hc.apache.org/httpcomponents-client-ga/ [HttpMime]: http://hc.apache.org/httpcomponents-client-ga/httpmime [json-java]: http://json.org/java/ -[javadoc]: http://soundcloud.github.com/java-api-wrapper/javadoc/1.1.0/com/soundcloud/api/package-summary.html +[javadoc]: http://soundcloud.github.com/java-api-wrapper/javadoc/1.1.1/com/soundcloud/api/package-summary.html [soundcloudapi-java]: http://code.google.com/p/soundcloudapi-java/ [soundcloudapi-java-annouce]: http://blog.soundcloud.com/2010/01/08/java-wrapper/ [CreateWrapper]: https://github.com/soundcloud/java-api-wrapper/blob/master/src/examples/java/com/soundcloud/api/examples/CreateWrapper.java @@ -266,7 +266,7 @@ See LICENSE for details. [SoundCloud Android]: https://market.android.com/details?id=com.soundcloud.android [register-app]: http://soundcloud.com/you/apps/new [Apache Maven]: http://maven.apache.org/ -[jar-all]: https://github.com/downloads/soundcloud/java-api-wrapper/java-api-wrapper-1.1.0-all.jar +[jar-all]: https://github.com/downloads/soundcloud/java-api-wrapper/java-api-wrapper-1.1.1-all.jar [downloads]: https://github.com/soundcloud/java-api-wrapper/archives/master [snapshots]: https://oss.sonatype.org/content/repositories/snapshots/com/soundcloud/java-api-wrapper/ [releases]: https://oss.sonatype.org/content/repositories/releases/com/soundcloud/java-api-wrapper/ diff --git a/build.gradle b/build.gradle index 6ab021a..967e0bb 100644 --- a/build.gradle +++ b/build.gradle @@ -4,7 +4,7 @@ apply plugin: 'eclipse' apply plugin: 'maven' sourceCompatibility = 1.6 -version = '1.1.1-SNAPSHOT' +version = '1.1.1' group = 'com.soundcloud' repositories { mavenCentral() } diff --git a/src/main/java/com/soundcloud/api/CloudAPI.java b/src/main/java/com/soundcloud/api/CloudAPI.java index 1e1ee02..dc7c23b 100644 --- a/src/main/java/com/soundcloud/api/CloudAPI.java +++ b/src/main/java/com/soundcloud/api/CloudAPI.java @@ -30,7 +30,7 @@ public interface CloudAPI { // other constants String REALM = "SoundCloud"; String OAUTH_SCHEME = "oauth"; - String VERSION = "1.1.0"; + String VERSION = "1.1.1"; String USER_AGENT = "SoundCloud Java Wrapper ("+VERSION+")"; diff --git a/src/main/java/com/soundcloud/api/package-info.java b/src/main/java/com/soundcloud/api/package-info.java index f567440..13715a8 100644 --- a/src/main/java/com/soundcloud/api/package-info.java +++ b/src/main/java/com/soundcloud/api/package-info.java @@ -7,6 +7,6 @@ * * @see com.soundcloud.api.ApiWrapper * @author Jan Berkel - * @version 1.1.0, 09/11/11 + * @version 1.1.1, 02/05/12 */ package com.soundcloud.api; From 7cce8e874607b7a9b89f43dd654317c72c30535b Mon Sep 17 00:00:00 2001 From: Jan Berkel Date: Wed, 2 May 2012 14:22:01 +0200 Subject: [PATCH 068/156] [maven-release-plugin] prepare release 1.1.1 --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index dbfb593..7026907 100644 --- a/pom.xml +++ b/pom.xml @@ -3,7 +3,7 @@ 4.0.0 com.soundcloud java-api-wrapper - 1.1.1-SNAPSHOT + 1.1.1 SoundCloud Java API wrapper SoundCloud Java API wrapper (OAuth2 only), works on Android https://github.com/soundcloud/java-api-wrapper#readme From a525d225bf4352d23008d83d7d894ddb0d87427b Mon Sep 17 00:00:00 2001 From: Jan Berkel Date: Wed, 2 May 2012 14:22:07 +0200 Subject: [PATCH 069/156] [maven-release-plugin] prepare for next development iteration --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 7026907..eb4c97f 100644 --- a/pom.xml +++ b/pom.xml @@ -3,7 +3,7 @@ 4.0.0 com.soundcloud java-api-wrapper - 1.1.1 + 1.1.2-SNAPSHOT SoundCloud Java API wrapper SoundCloud Java API wrapper (OAuth2 only), works on Android https://github.com/soundcloud/java-api-wrapper#readme From 590cd3a1adf7fb958eba1c514828cca3c1ee2f1a Mon Sep 17 00:00:00 2001 From: Jan Berkel Date: Wed, 9 May 2012 23:25:17 +0200 Subject: [PATCH 070/156] Add build status --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index 475cfa6..accd48a 100644 --- a/README.md +++ b/README.md @@ -249,6 +249,8 @@ Includes portions of code (c) 2010 Xtreme Labs and Pivotal Labs and (c) 2009 urb See LICENSE for details. +[![Build Status](https://secure.travis-ci.org/soundcloud/java-api-wrapper.png?branch=master)](http://travis-ci.org/soundcloud/java-api-wrapper) + [gradle]: http://www.gradle.org/ [urbanstew]: http://urbanstew.org/ [Apache HttpClient]: http://hc.apache.org/httpcomponents-client-ga/ From f0a2b667d90294e6bb435fe81ba4cad5647ae887 Mon Sep 17 00:00:00 2001 From: Jan Berkel Date: Fri, 25 May 2012 17:38:04 +0200 Subject: [PATCH 071/156] Support for overwriting parameters --- src/main/java/com/soundcloud/api/Request.java | 26 +++++++++++++++++++ .../java/com/soundcloud/api/RequestTest.java | 15 +++++++++++ 2 files changed, 41 insertions(+) diff --git a/src/main/java/com/soundcloud/api/Request.java b/src/main/java/com/soundcloud/api/Request.java index 69b1c63..c853bfb 100644 --- a/src/main/java/com/soundcloud/api/Request.java +++ b/src/main/java/com/soundcloud/api/Request.java @@ -138,6 +138,32 @@ public Request add(String name, Object value) { return this; } + + /** + * Sets a new parameter, overwriting previous value. + * @param name the name + * @param value the value + * @return this + */ + public Request set(String name, Object value) { + return clear(name).add(name, value); + } + + /** + * Clears a parameter + * @param name name of the parameter + * @return this + */ + public Request clear(String name) { + Iterator it = mParams.iterator(); + while (it.hasNext()) { + if (it.next().getName().equals(name)) { + it.remove(); + } + } + return this; + } + /** * @param args a list of arguments * @return this diff --git a/src/test/java/com/soundcloud/api/RequestTest.java b/src/test/java/com/soundcloud/api/RequestTest.java index 9ddc763..5b37426 100644 --- a/src/test/java/com/soundcloud/api/RequestTest.java +++ b/src/test/java/com/soundcloud/api/RequestTest.java @@ -59,6 +59,21 @@ public void shouldSupportWith() throws Exception { assertThat(p.queryString(), equalTo("foo=100&baz=22.3&baz=66")); } + @Test + public void shouldSupportOverwritingParameters() { + Request r = new Request(); + r.add("foo", 1) + .add("foo", 2); + + assertThat(r.queryString(), equalTo("foo=1&foo=2")); + + r.set("foo", 3); + assertThat(r.queryString(), equalTo("foo=3")); + + r.clear("foo"); + assertThat(r.queryString(), equalTo("")); + } + @Test public void shouldCopyRequestWithNewResource() throws Exception { Request p = new Request().with("foo", 100, "baz", 22.3f); From c26a888cf3693bd05fcdacfbd38b13081f84d22a Mon Sep 17 00:00:00 2001 From: Jan Berkel Date: Fri, 25 May 2012 17:39:13 +0200 Subject: [PATCH 072/156] Bump version --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index 967e0bb..96f747e 100644 --- a/build.gradle +++ b/build.gradle @@ -4,7 +4,7 @@ apply plugin: 'eclipse' apply plugin: 'maven' sourceCompatibility = 1.6 -version = '1.1.1' +version = '1.1.2-SNAPSHOT' group = 'com.soundcloud' repositories { mavenCentral() } From 268d0f03a2547a3b7c0a540c0ffc7cdbfd14822a Mon Sep 17 00:00:00 2001 From: Jan Berkel Date: Fri, 25 May 2012 19:16:49 +0200 Subject: [PATCH 073/156] Public formatRange --- src/main/java/com/soundcloud/api/Request.java | 2 +- src/test/java/com/soundcloud/api/RequestTest.java | 13 +++++++++++++ 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/src/main/java/com/soundcloud/api/Request.java b/src/main/java/com/soundcloud/api/Request.java index c853bfb..cd317ac 100644 --- a/src/main/java/com/soundcloud/api/Request.java +++ b/src/main/java/com/soundcloud/api/Request.java @@ -424,7 +424,7 @@ public T buildRequest(Class method) { } } - static String formatRange(long... range) { + public static String formatRange(long... range) { switch (range.length) { case 0: return "bytes=0-"; case 1: diff --git a/src/test/java/com/soundcloud/api/RequestTest.java b/src/test/java/com/soundcloud/api/RequestTest.java index 5b37426..f30a0f7 100644 --- a/src/test/java/com/soundcloud/api/RequestTest.java +++ b/src/test/java/com/soundcloud/api/RequestTest.java @@ -16,6 +16,7 @@ import org.apache.http.entity.StringEntity; import org.apache.http.entity.mime.MultipartEntity; import org.hamcrest.CoreMatchers; +import org.junit.Ignore; import org.junit.Test; import java.io.ByteArrayInputStream; @@ -45,6 +46,18 @@ public void shouldGenerateUrlWithParameters() throws Exception { assertThat(p.toUrl("http://foo.com"), equalTo("http://foo.com?foo=100&baz=22.3")); } + + @Test @Ignore + public void shouldNotModifyOriginalRequest() throws Exception { + String url = "http://ec-media.soundcloud.com/SdPniMt7cZzj.128.mp3?ff61182e3c2ecefa438cd02102d0e385713f0c1f" + + "af3b0339595660fd0603ed1dd95c308fdf4dfe37b272d5fc302cd60875f62fda2557f961990ca6e770fdb81c291f729" + + "2cb&AWSAccessKeyId=AKIAJBHW5FB4ERKUQUOQ&Expires=1337966965&Signature=dFluZNnDMGZiXCACfRru9VrB%2" + + "Bbg%3D"; + + Request r = new Request(url); + assertThat(r.toUrl(), equalTo(url)); + } + @Test public void shouldHaveSizeMethod() throws Exception { Request p = new Request().with("foo", 100, "baz", 22.3f); From 732e7fd9cfc4824b9eaff0eed3d86d849b14308e Mon Sep 17 00:00:00 2001 From: Jan Berkel Date: Tue, 3 Jul 2012 17:37:34 -0700 Subject: [PATCH 074/156] Add support for default parameters --- .../java/com/soundcloud/api/ApiWrapper.java | 34 +++++++++++- .../com/soundcloud/api/ApiWrapperTest.java | 55 +++++++++++++++++++ 2 files changed, 88 insertions(+), 1 deletion(-) diff --git a/src/main/java/com/soundcloud/api/ApiWrapper.java b/src/main/java/com/soundcloud/api/ApiWrapper.java index b605ced..e7dc81e 100644 --- a/src/main/java/com/soundcloud/api/ApiWrapper.java +++ b/src/main/java/com/soundcloud/api/ApiWrapper.java @@ -5,6 +5,7 @@ import org.apache.http.HttpHost; import org.apache.http.HttpResponse; import org.apache.http.HttpStatus; +import org.apache.http.NameValuePair; import org.apache.http.auth.AUTH; import org.apache.http.auth.AuthScope; import org.apache.http.client.AuthenticationHandler; @@ -106,6 +107,7 @@ public class ApiWrapper implements CloudAPI, Serializable { /** debug request details to stderr */ public boolean debugRequests; + /** * Constructs a new ApiWrapper instance. * @@ -146,7 +148,7 @@ public ApiWrapper(String clientId, @Override public Token authorizationCode(String code, String... scopes) throws IOException { if (code == null) { - throw new IllegalArgumentException("username or password is null"); + throw new IllegalArgumentException("code is null"); } final Request request = addScope(Request.to(Endpoints.TOKEN).with( "grant_type", AUTHORIZATION_CODE, @@ -566,6 +568,14 @@ public HttpResponse safeExecute(HttpHost target, HttpUriRequest request) throws } protected HttpResponse execute(Request req, Class reqType) throws IOException { + Request defaults = ApiWrapper.defaultParams.get(); + if (defaults != null && !defaults.getParams().isEmpty()) { + // copy + merge in default parameters + for (NameValuePair nvp : defaults) { + req = new Request(req); + req.add(nvp.getName(), nvp.getValue()); + } + } if (debugRequests) System.err.println(reqType.getSimpleName()+" "+req); return execute(req.buildRequest(reqType)); } @@ -681,4 +691,26 @@ protected RequestDirector getRequestDirector(HttpRequestExecutor requestExec, stateHandler, params); } + private static final ThreadLocal defaultParams = new ThreadLocal() { + @Override protected Request initialValue() { + return new Request(); + } + }; + + /** + * Adds a default parameter which will get added to all requests in this thread. + * Use this method carefully since it might lead to unexpected side-effects. + * @param name the name of the parameter + * @param value the value of the parameter. + */ + public static void setDefaultParameter(String name, String value) { + defaultParams.get().set(name, value); + } + + /** + * Clears the default parameters. + */ + public static void clearDefaultParameters() { + defaultParams.remove(); + } } diff --git a/src/test/java/com/soundcloud/api/ApiWrapperTest.java b/src/test/java/com/soundcloud/api/ApiWrapperTest.java index ea54d4f..506410f 100644 --- a/src/test/java/com/soundcloud/api/ApiWrapperTest.java +++ b/src/test/java/com/soundcloud/api/ApiWrapperTest.java @@ -515,4 +515,59 @@ public HttpClient getHttpClient() { verify(client, times(2)).execute(any(HttpHost.class), any(HttpUriRequest.class)); } } + + @Test + public void testAddDefaultParameters() throws Exception { + layer.addHttpResponseRule("/foo", "Hi"); + layer.addHttpResponseRule("/foo?t=1", "Hi t1"); + layer.addHttpResponseRule("/foo?t=2", "Hi t2"); + + final Request foo = Request.to("/foo"); + for (int i = 0; i < 1000; i++) { + final Exception throwable[] = new Exception[2]; + Thread t1 = new Thread("t1") { + @Override + public void run() { + ApiWrapper.setDefaultParameter("t", "1"); + try { + assertEquals("Hi t1", Http.getString(api.get(foo))); + } catch (Exception e) { + throwable[0] = e; + } + ApiWrapper.clearDefaultParameters(); + try { + assertEquals("Hi", Http.getString(api.get(foo))); + } catch (Exception e) { + throwable[0] = e; + } + } + }; + + Thread t2 = new Thread("t2") { + @Override + public void run() { + ApiWrapper.setDefaultParameter("t", "2"); + try { + assertEquals("Hi t2", Http.getString(api.get(foo))); + } catch (Exception e) { + throwable[1] = e; + } + ApiWrapper.clearDefaultParameters(); + try { + assertEquals("Hi", Http.getString(api.get(foo))); + } catch (Exception e) { + throwable[1] = e; + } + } + }; + t1.start(); + t2.start(); + t1.join(); + t2.join(); + if (throwable[0] != null) throw throwable[0]; + if (throwable[1] != null) throw throwable[1]; + + assertEquals("Hi", Http.getString(api.get(foo))); + } + } } From 431f15efbc6c1d198d26cbc74487e69d2b28ac87 Mon Sep 17 00:00:00 2001 From: Jan Berkel Date: Tue, 3 Jul 2012 18:06:55 -0700 Subject: [PATCH 075/156] Add logger extension point --- src/main/java/com/soundcloud/api/ApiWrapper.java | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/main/java/com/soundcloud/api/ApiWrapper.java b/src/main/java/com/soundcloud/api/ApiWrapper.java index e7dc81e..ba8340a 100644 --- a/src/main/java/com/soundcloud/api/ApiWrapper.java +++ b/src/main/java/com/soundcloud/api/ApiWrapper.java @@ -576,11 +576,15 @@ protected HttpResponse execute(Request req, Class req req.add(nvp.getName(), nvp.getValue()); } } - if (debugRequests) System.err.println(reqType.getSimpleName()+" "+req); + logRequest(reqType, req); return execute(req.buildRequest(reqType)); } + protected void logRequest( Class reqType, Request request) { + if (debugRequests) System.err.println(reqType.getSimpleName()+" "+request); + } + protected HttpHost determineTarget(HttpUriRequest request) { // A null target may be acceptable if there is a default target. // Otherwise, the null target is detected in the director. From 9e69be0c67777cdf5537b94bbabb6584baa9a83f Mon Sep 17 00:00:00 2001 From: Jan Berkel Date: Thu, 4 Oct 2012 13:03:28 +0200 Subject: [PATCH 076/156] Explictly set port numbers fix for http://code.google.com/p/android/issues/detail?id=2690 --- src/main/java/com/soundcloud/api/Env.java | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/main/java/com/soundcloud/api/Env.java b/src/main/java/com/soundcloud/api/Env.java index b46b938..6bb2ac4 100644 --- a/src/main/java/com/soundcloud/api/Env.java +++ b/src/main/java/com/soundcloud/api/Env.java @@ -19,11 +19,11 @@ public enum Env { * @param authResourceHost the authentication resource host */ Env(String resourceHost, String authResourceHost) { - this.resourceHost = new HttpHost(resourceHost, -1, "http"); - sslResourceHost = new HttpHost(resourceHost, -1, "https"); + this.resourceHost = new HttpHost(resourceHost, 80, "http"); + sslResourceHost = new HttpHost(resourceHost, 443, "https"); - this.authResourceHost = new HttpHost(authResourceHost, -1, "http"); - sslAuthResourceHost = new HttpHost(authResourceHost, -1, "https"); + this.authResourceHost = new HttpHost(authResourceHost, 80, "http"); + sslAuthResourceHost = new HttpHost(authResourceHost, 443, "https"); } public HttpHost getResourceHost(boolean secure) { From ceefce703cb248581a2f26a48a7eec719f40753c Mon Sep 17 00:00:00 2001 From: Jan Berkel Date: Thu, 4 Oct 2012 13:37:32 +0200 Subject: [PATCH 077/156] Explicitly specify ports --- CHANGES.md | 5 +++++ .../java/com/soundcloud/api/ApiWrapper.java | 4 ++-- src/main/java/com/soundcloud/api/Env.java | 19 +++++++++++++++++++ .../api/CloudAPIIntegrationTest.java | 1 + src/test/java/com/soundcloud/api/EnvTest.java | 10 ++++++++++ 5 files changed, 37 insertions(+), 2 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index ef8a546..2a92eaa 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,3 +1,8 @@ +## 1.1.2 2012-10-04 + + * Compatibility fixes with broken httpclient versions used in Android pre-gingerbread + * Add support for default parameters + ## 1.1.1 2012-04-30 * Respect system proxy settings diff --git a/src/main/java/com/soundcloud/api/ApiWrapper.java b/src/main/java/com/soundcloud/api/ApiWrapper.java index ba8340a..03d02d2 100644 --- a/src/main/java/com/soundcloud/api/ApiWrapper.java +++ b/src/main/java/com/soundcloud/api/ApiWrapper.java @@ -242,8 +242,8 @@ public Token extensionGrantType(String grantType, String... scopes) throws IOExc * @return a valid URI */ public URI getURI(Request request, boolean api, boolean secure) { - return URI.create((api ? env.getResourceHost(secure) : env.getAuthResourceHost(secure)).toURI()) - .resolve(request.toUrl()); + final URI uri = api ? env.getResourceURI(secure) : env.getAuthResourceURI(secure); + return uri.resolve(request.toUrl()); } /** diff --git a/src/main/java/com/soundcloud/api/Env.java b/src/main/java/com/soundcloud/api/Env.java index 6bb2ac4..f64ed6d 100644 --- a/src/main/java/com/soundcloud/api/Env.java +++ b/src/main/java/com/soundcloud/api/Env.java @@ -2,6 +2,9 @@ import org.apache.http.HttpHost; +import java.net.URI; +import java.net.URISyntaxException; + /** * The environment to operate against. * Use SANDBOX for testing your app, and LIVE for production applications. @@ -34,9 +37,25 @@ public HttpHost getAuthResourceHost(boolean secure) { return secure ? sslAuthResourceHost : authResourceHost; } + public URI getResourceURI(boolean secure) { + return hostToUri(getResourceHost(secure)); + } + + public URI getAuthResourceURI(boolean secure) { + return hostToUri(getAuthResourceHost(secure)); + } + public boolean isApiHost(HttpHost host) { return ("http".equals(host.getSchemeName()) || "https".equals(host.getSchemeName())) && resourceHost.getHostName().equals(host.getHostName()); } + + private static URI hostToUri(HttpHost host) { + try { + return new URI(host.getSchemeName(), host.getHostName(), null, null); + } catch (URISyntaxException ignored) { + throw new RuntimeException(); + } + } } diff --git a/src/test/java/com/soundcloud/api/CloudAPIIntegrationTest.java b/src/test/java/com/soundcloud/api/CloudAPIIntegrationTest.java index d9e9eac..78c61f2 100644 --- a/src/test/java/com/soundcloud/api/CloudAPIIntegrationTest.java +++ b/src/test/java/com/soundcloud/api/CloudAPIIntegrationTest.java @@ -23,6 +23,7 @@ import java.util.concurrent.BrokenBarrierException; import java.util.concurrent.CyclicBarrier; +@Ignore // sandbox is kaputt public class CloudAPIIntegrationTest implements Params.Track, Endpoints { // http://sandbox-soundcloud.com/you/apps/java-api-wrapper-test-app // user: api-testing diff --git a/src/test/java/com/soundcloud/api/EnvTest.java b/src/test/java/com/soundcloud/api/EnvTest.java index bc47378..28ebb44 100644 --- a/src/test/java/com/soundcloud/api/EnvTest.java +++ b/src/test/java/com/soundcloud/api/EnvTest.java @@ -1,5 +1,6 @@ package com.soundcloud.api; +import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; @@ -14,4 +15,13 @@ public void testIsApiHost() throws Exception { assertTrue(Env.LIVE.isApiHost(new HttpHost("api.soundcloud.com", 443, "https"))); assertFalse(Env.LIVE.isApiHost(new HttpHost("foo.soundcloud.com", 443, "https"))); } + + @Test + public void shouldHostsShouldExplicitlySpecifyPorts() throws Exception { + assertEquals(80, Env.LIVE.authResourceHost.getPort()); + assertEquals(443, Env.LIVE.sslAuthResourceHost.getPort()); + + assertEquals(80, Env.LIVE.resourceHost.getPort()); + assertEquals(443, Env.LIVE.sslResourceHost.getPort()); + } } From ab62b7b803656368786e58f9ab7c835ec36e4d0d Mon Sep 17 00:00:00 2001 From: Jan Berkel Date: Thu, 4 Oct 2012 13:39:15 +0200 Subject: [PATCH 078/156] Bump version numbers --- README.md | 4 ++-- build.gradle | 2 +- src/main/java/com/soundcloud/api/CloudAPI.java | 2 +- src/main/java/com/soundcloud/api/package-info.java | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index accd48a..3180fe4 100644 --- a/README.md +++ b/README.md @@ -256,7 +256,7 @@ See LICENSE for details. [Apache HttpClient]: http://hc.apache.org/httpcomponents-client-ga/ [HttpMime]: http://hc.apache.org/httpcomponents-client-ga/httpmime [json-java]: http://json.org/java/ -[javadoc]: http://soundcloud.github.com/java-api-wrapper/javadoc/1.1.1/com/soundcloud/api/package-summary.html +[javadoc]: http://soundcloud.github.com/java-api-wrapper/javadoc/1.1.2/com/soundcloud/api/package-summary.html [soundcloudapi-java]: http://code.google.com/p/soundcloudapi-java/ [soundcloudapi-java-annouce]: http://blog.soundcloud.com/2010/01/08/java-wrapper/ [CreateWrapper]: https://github.com/soundcloud/java-api-wrapper/blob/master/src/examples/java/com/soundcloud/api/examples/CreateWrapper.java @@ -268,7 +268,7 @@ See LICENSE for details. [SoundCloud Android]: https://market.android.com/details?id=com.soundcloud.android [register-app]: http://soundcloud.com/you/apps/new [Apache Maven]: http://maven.apache.org/ -[jar-all]: https://github.com/downloads/soundcloud/java-api-wrapper/java-api-wrapper-1.1.1-all.jar +[jar-all]: https://github.com/downloads/soundcloud/java-api-wrapper/java-api-wrapper-1.1.2-all.jar [downloads]: https://github.com/soundcloud/java-api-wrapper/archives/master [snapshots]: https://oss.sonatype.org/content/repositories/snapshots/com/soundcloud/java-api-wrapper/ [releases]: https://oss.sonatype.org/content/repositories/releases/com/soundcloud/java-api-wrapper/ diff --git a/build.gradle b/build.gradle index 96f747e..a860c83 100644 --- a/build.gradle +++ b/build.gradle @@ -4,7 +4,7 @@ apply plugin: 'eclipse' apply plugin: 'maven' sourceCompatibility = 1.6 -version = '1.1.2-SNAPSHOT' +version = '1.1.2' group = 'com.soundcloud' repositories { mavenCentral() } diff --git a/src/main/java/com/soundcloud/api/CloudAPI.java b/src/main/java/com/soundcloud/api/CloudAPI.java index dc7c23b..7744271 100644 --- a/src/main/java/com/soundcloud/api/CloudAPI.java +++ b/src/main/java/com/soundcloud/api/CloudAPI.java @@ -30,7 +30,7 @@ public interface CloudAPI { // other constants String REALM = "SoundCloud"; String OAUTH_SCHEME = "oauth"; - String VERSION = "1.1.1"; + String VERSION = "1.1.2"; String USER_AGENT = "SoundCloud Java Wrapper ("+VERSION+")"; diff --git a/src/main/java/com/soundcloud/api/package-info.java b/src/main/java/com/soundcloud/api/package-info.java index 13715a8..4f24a48 100644 --- a/src/main/java/com/soundcloud/api/package-info.java +++ b/src/main/java/com/soundcloud/api/package-info.java @@ -7,6 +7,6 @@ * * @see com.soundcloud.api.ApiWrapper * @author Jan Berkel - * @version 1.1.1, 02/05/12 + * @version 1.1.2, 04/10/12 */ package com.soundcloud.api; From df0042f88ba2ad5d0229d248aca22fedac011b95 Mon Sep 17 00:00:00 2001 From: Jan Berkel Date: Thu, 4 Oct 2012 13:47:45 +0200 Subject: [PATCH 079/156] Fix writePom --- build.gradle | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/build.gradle b/build.gradle index a860c83..c9e52c8 100644 --- a/build.gradle +++ b/build.gradle @@ -27,7 +27,8 @@ dependencies { } uploadArchives { - repositories.mavenDeployer { + repositories { + mavenDeployer { snapshotRepository(url: 'https://oss.sonatype.org/content/repositories/snapshots', id: 'sonatype-nexus-snapshots') { authentication(getAuth('sonatype-nexus-snapshots')) @@ -53,6 +54,7 @@ uploadArchives { withXml { xml -> new XmlParser().parse(new File("pom-include.xml")).children().each { kid -> xml.asNode().append(kid) } } + } } } } @@ -83,7 +85,7 @@ example('uploadFile', 'UploadFile', { file }) example('dumpToken', 'DumpToken', { [] }) task writePom << { - repositories.mavenDeployer().getPom().writeTo("pom.xml") + uploadArchives.repositories.mavenDeployer().getPom().writeTo("pom.xml") } task doc(type: Javadoc) { From 3d7a8f5a5d12bed8b5eb69458e23315abf31a9c7 Mon Sep 17 00:00:00 2001 From: Jan Berkel Date: Thu, 4 Oct 2012 13:47:53 +0200 Subject: [PATCH 080/156] POM update --- pom.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pom.xml b/pom.xml index eb4c97f..d9879c2 100644 --- a/pom.xml +++ b/pom.xml @@ -1,9 +1,9 @@ - + 4.0.0 com.soundcloud java-api-wrapper - 1.1.2-SNAPSHOT + 1.1.2 SoundCloud Java API wrapper SoundCloud Java API wrapper (OAuth2 only), works on Android https://github.com/soundcloud/java-api-wrapper#readme From 2cadb08b51906989c13180a92d6c1d7395367cf5 Mon Sep 17 00:00:00 2001 From: Jan Berkel Date: Thu, 4 Oct 2012 13:48:45 +0200 Subject: [PATCH 081/156] snap --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index d9879c2..1060889 100644 --- a/pom.xml +++ b/pom.xml @@ -3,7 +3,7 @@ 4.0.0 com.soundcloud java-api-wrapper - 1.1.2 + 1.1.2-SNAPSHOT SoundCloud Java API wrapper SoundCloud Java API wrapper (OAuth2 only), works on Android https://github.com/soundcloud/java-api-wrapper#readme From e070fa165222cea4d9cdfd9d553011840067e2cf Mon Sep 17 00:00:00 2001 From: Jan Berkel Date: Thu, 4 Oct 2012 13:49:44 +0200 Subject: [PATCH 082/156] [maven-release-plugin] prepare release 1.1.2 --- pom.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pom.xml b/pom.xml index 1060889..1899186 100644 --- a/pom.xml +++ b/pom.xml @@ -1,9 +1,9 @@ - + 4.0.0 com.soundcloud java-api-wrapper - 1.1.2-SNAPSHOT + 1.1.2 SoundCloud Java API wrapper SoundCloud Java API wrapper (OAuth2 only), works on Android https://github.com/soundcloud/java-api-wrapper#readme From 592520c6fa3dba24d3fafdd7a835b7d8caadf53a Mon Sep 17 00:00:00 2001 From: Jan Berkel Date: Thu, 4 Oct 2012 13:49:51 +0200 Subject: [PATCH 083/156] [maven-release-plugin] prepare for next development iteration --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 1899186..af3db84 100644 --- a/pom.xml +++ b/pom.xml @@ -3,7 +3,7 @@ 4.0.0 com.soundcloud java-api-wrapper - 1.1.2 + 1.1.3-SNAPSHOT SoundCloud Java API wrapper SoundCloud Java API wrapper (OAuth2 only), works on Android https://github.com/soundcloud/java-api-wrapper#readme From 9fee52d76f5beae0bc543312864d7a9b5d3750bb Mon Sep 17 00:00:00 2001 From: Jan Berkel Date: Fri, 5 Oct 2012 13:44:30 +0200 Subject: [PATCH 084/156] Catch another exception --- src/main/java/com/soundcloud/api/ApiWrapper.java | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/main/java/com/soundcloud/api/ApiWrapper.java b/src/main/java/com/soundcloud/api/ApiWrapper.java index 03d02d2..150379c 100644 --- a/src/main/java/com/soundcloud/api/ApiWrapper.java +++ b/src/main/java/com/soundcloud/api/ApiWrapper.java @@ -564,6 +564,14 @@ public HttpResponse safeExecute(HttpHost target, HttpUriRequest request) throws // cf. http://code.google.com/p/android/issues/detail?id=2690 request.abort(); throw new BrokenHttpClientException(e); + } catch (ArrayIndexOutOfBoundsException e) { + // Caused by: java.lang.ArrayIndexOutOfBoundsException: length=7; index=-9 + // org.apache.harmony.security.asn1.DerInputStream.readBitString(DerInputStream.java:72)) + // org.apache.harmony.security.asn1.ASN1BitString.decode(ASN1BitString.java:64) + // ... + // org.apache.http.conn.ssl.SSLSocketFactory.createSocket(SSLSocketFactory.java:375) + request.abort(); + throw new BrokenHttpClientException(e); } } From 1eb679be71d2757e1d65677e8d7f1bafbc396607 Mon Sep 17 00:00:00 2001 From: Jan Berkel Date: Fri, 5 Oct 2012 13:45:42 +0200 Subject: [PATCH 085/156] Bump version --- CHANGES.md | 4 ++++ build.gradle | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/CHANGES.md b/CHANGES.md index 2a92eaa..8c9f2c3 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,3 +1,7 @@ +## 1.1.3-SNAPSHOT + + * Handle more broken HTTP client behaviour + ## 1.1.2 2012-10-04 * Compatibility fixes with broken httpclient versions used in Android pre-gingerbread diff --git a/build.gradle b/build.gradle index c9e52c8..9451586 100644 --- a/build.gradle +++ b/build.gradle @@ -4,7 +4,7 @@ apply plugin: 'eclipse' apply plugin: 'maven' sourceCompatibility = 1.6 -version = '1.1.2' +version = '1.1.3-SNAPSHOT' group = 'com.soundcloud' repositories { mavenCentral() } From ee538f2c6440260b4387726cd1ad7d00ae5312e6 Mon Sep 17 00:00:00 2001 From: Jan Berkel Date: Mon, 5 Nov 2012 17:49:22 +0100 Subject: [PATCH 086/156] Remove sandbox --- CHANGES.md | 1 + .../api/examples/CreateWrapper.java | 6 ++--- .../api/examples/FacebookConnect.java | 4 ++-- .../java/com/soundcloud/api/ApiWrapper.java | 17 ++++--------- src/main/java/com/soundcloud/api/Env.java | 1 + .../com/soundcloud/api/ApiWrapperTest.java | 24 +++++++++---------- .../api/CloudAPIIntegrationTest.java | 3 +-- 7 files changed, 24 insertions(+), 32 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 8c9f2c3..d41491a 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,6 +1,7 @@ ## 1.1.3-SNAPSHOT * Handle more broken HTTP client behaviour + * Remove sandbox environment (it is no longer supported) ## 1.1.2 2012-10-04 diff --git a/src/examples/java/com/soundcloud/api/examples/CreateWrapper.java b/src/examples/java/com/soundcloud/api/examples/CreateWrapper.java index a40594c..a7e76d4 100644 --- a/src/examples/java/com/soundcloud/api/examples/CreateWrapper.java +++ b/src/examples/java/com/soundcloud/api/examples/CreateWrapper.java @@ -1,7 +1,6 @@ package com.soundcloud.api.examples; import com.soundcloud.api.ApiWrapper; -import com.soundcloud.api.Env; import com.soundcloud.api.Token; import java.io.File; @@ -16,15 +15,14 @@ public final class CreateWrapper { public static void main(String[] args) throws Exception { if (args.length < 4) { - System.err.println("CreateWrapper client_id client_secret login password [live|sandbox] [scope]"); + System.err.println("CreateWrapper client_id client_secret login password"); System.exit(1); } else { final ApiWrapper wrapper = new ApiWrapper( args[0] /* client_id */, args[1] /* client_secret */, null /* redirect URI */, - null /* token */, - args.length == 5 ? Env.valueOf(args[4].toUpperCase()) : Env.LIVE); + null /* token */); Token token; if (args.length < 6) { diff --git a/src/examples/java/com/soundcloud/api/examples/FacebookConnect.java b/src/examples/java/com/soundcloud/api/examples/FacebookConnect.java index 5fc4525..5b5f3a1 100644 --- a/src/examples/java/com/soundcloud/api/examples/FacebookConnect.java +++ b/src/examples/java/com/soundcloud/api/examples/FacebookConnect.java @@ -38,8 +38,8 @@ public static void main(String[] args) throws IOException { CLIENT_ID, CLIENT_SECRET, REDIRECT_URI, - null /* token */, - Env.SANDBOX); + null /* token */); + // generate the URL the user needs to open in the browser URI url = wrapper.authorizationCodeUrl(Endpoints.FACEBOOK_CONNECT, Token.SCOPE_NON_EXPIRING); diff --git a/src/main/java/com/soundcloud/api/ApiWrapper.java b/src/main/java/com/soundcloud/api/ApiWrapper.java index 150379c..3d1a052 100644 --- a/src/main/java/com/soundcloud/api/ApiWrapper.java +++ b/src/main/java/com/soundcloud/api/ApiWrapper.java @@ -79,14 +79,14 @@ * HttpResponse response = wrapper.get(Request.to("/tracks")); * * - * @see CloudAPI + * @see Using the SoundCloud API */ public class ApiWrapper implements CloudAPI, Serializable { private static final long serialVersionUID = 3662083416905771921L; public static final String DEFAULT_CONTENT_TYPE = "application/json"; - /** The current environment */ - public final Env env; + /** The current environment, only live possible for now */ + public final Env env = Env.LIVE; private Token mToken; private final String mClientId, mClientSecret; @@ -115,19 +115,16 @@ public class ApiWrapper implements CloudAPI, Serializable { * @param clientSecret the application client secret * @param redirectUri the registered redirect url, or null * @param token an valid token, or null if not known - * @param env the environment to use (LIVE/SANDBOX) - * @see API documentation + * @see API authentication documentation */ public ApiWrapper(String clientId, String clientSecret, URI redirectUri, - Token token, - Env env) { + Token token) { mClientId = clientId; mClientSecret = clientSecret; mRedirectUri = redirectUri; mToken = token == null ? new Token(null, null) : token; - this.env = env; } @Override public Token login(String username, String password, String... scopes) throws IOException { @@ -383,10 +380,6 @@ public HttpClient getHttpClient() { final SchemeRegistry registry = new SchemeRegistry(); registry.register(new Scheme("http", getSocketFactory(), 80)); final SSLSocketFactory sslFactory = getSSLSocketFactory(); - if (env == Env.SANDBOX) { - // disable strict checks on sandbox XXX remove when certificate is fixed - sslFactory.setHostnameVerifier(SSLSocketFactory.ALLOW_ALL_HOSTNAME_VERIFIER); - } registry.register(new Scheme("https", sslFactory, 443)); httpClient = new DefaultHttpClient( new ThreadSafeClientConnManager(params, registry), diff --git a/src/main/java/com/soundcloud/api/Env.java b/src/main/java/com/soundcloud/api/Env.java index f64ed6d..2b1aead 100644 --- a/src/main/java/com/soundcloud/api/Env.java +++ b/src/main/java/com/soundcloud/api/Env.java @@ -13,6 +13,7 @@ public enum Env { /** The main production site, http://soundcloud.com */ LIVE("api.soundcloud.com", "soundcloud.com"), /** For testing, http://sandbox-soundcloud.com */ + @Deprecated SANDBOX("api.sandbox-soundcloud.com", "sandbox-soundcloud.com"); public final HttpHost resourceHost, sslResourceHost, authResourceHost, sslAuthResourceHost; diff --git a/src/test/java/com/soundcloud/api/ApiWrapperTest.java b/src/test/java/com/soundcloud/api/ApiWrapperTest.java index 506410f..b2888f3 100644 --- a/src/test/java/com/soundcloud/api/ApiWrapperTest.java +++ b/src/test/java/com/soundcloud/api/ApiWrapperTest.java @@ -49,7 +49,7 @@ public class ApiWrapperTest { final FakeHttpLayer layer = new FakeHttpLayer(); @Before public void setup() { - api = new ApiWrapper("invalid", "invalid", URI.create("redirect://me"), null, Env.SANDBOX) { + api = new ApiWrapper("invalid", "invalid", URI.create("redirect://me"), null) { private static final long serialVersionUID = 12345; // silence warnings @Override protected RequestDirector getRequestDirector(HttpRequestExecutor requestExec, @@ -311,7 +311,7 @@ public void testGetOAuthHeaderNullToken() throws Exception { public void shouldGenerateUrlWithoutParameters() throws Exception { assertThat( api.getURI(new Request("/my-resource"), true, true).toString(), - equalTo("https://api.sandbox-soundcloud.com/my-resource") + equalTo("https://api.soundcloud.com/my-resource") ); } @@ -319,7 +319,7 @@ public void shouldGenerateUrlWithoutParameters() throws Exception { public void shouldGenerateUrlWithoutSSL() throws Exception { assertThat( api.getURI(new Request("/my-resource"), true, false).toString(), - equalTo("http://api.sandbox-soundcloud.com/my-resource") + equalTo("http://api.soundcloud.com/my-resource") ); } @@ -327,7 +327,7 @@ public void shouldGenerateUrlWithoutSSL() throws Exception { public void shouldGenerateUrlWithParameters() throws Exception { assertThat( api.getURI(Request.to("/my-resource").with("foo", "bar"), true, true).toString(), - equalTo("https://api.sandbox-soundcloud.com/my-resource?foo=bar") + equalTo("https://api.soundcloud.com/my-resource?foo=bar") ); } @@ -335,7 +335,7 @@ public void shouldGenerateUrlWithParameters() throws Exception { public void shouldGenerateUrlForWebHost() throws Exception { assertThat( api.getURI(Request.to("/my-resource"), false, true).toString(), - equalTo("https://sandbox-soundcloud.com/my-resource") + equalTo("https://soundcloud.com/my-resource") ); } @@ -344,7 +344,7 @@ public void shouldGenerateUrlForWebHost() throws Exception { public void shouldGenerateURIForLoginAuthCode() throws Exception { assertThat( api.authorizationCodeUrl().toString(), - equalTo("https://sandbox-soundcloud.com/connect"+ + equalTo("https://soundcloud.com/connect"+ "?redirect_uri=redirect%3A%2F%2Fme&client_id=invalid&response_type=code") ); } @@ -354,7 +354,7 @@ public void shouldGenerateURIForLoginAuthCode() throws Exception { public void shouldGenerateURIForLoginAuthCodeWithDifferentEndPoint() throws Exception { assertThat( api.authorizationCodeUrl(Endpoints.FACEBOOK_CONNECT).toString(), - equalTo("https://sandbox-soundcloud.com/connect/via/facebook"+ + equalTo("https://soundcloud.com/connect/via/facebook"+ "?redirect_uri=redirect%3A%2F%2Fme&client_id=invalid&response_type=code") ); } @@ -363,7 +363,7 @@ public void shouldGenerateURIForLoginAuthCodeWithDifferentEndPoint() throws Exce public void shouldIncludeScopeInAuthorizationUrl() throws Exception { assertThat( api.authorizationCodeUrl(Endpoints.FACEBOOK_CONNECT, Token.SCOPE_NON_EXPIRING).toString(), - equalTo("https://sandbox-soundcloud.com/connect/via/facebook"+ + equalTo("https://soundcloud.com/connect/via/facebook"+ "?redirect_uri=redirect%3A%2F%2Fme&client_id=invalid&response_type=code&scope=non-expiring") ); } @@ -410,7 +410,7 @@ public void shouldCallTokenStateListenerWhenTokenIsRefreshed() throws Exception @Test public void shouldSerializeAndDeserializeWrapper() throws Exception { - ApiWrapper wrapper = new ApiWrapper("client", "secret", null, new Token("1", "2"), Env.SANDBOX); + ApiWrapper wrapper = new ApiWrapper("client", "secret", null, new Token("1", "2")); File ser = File.createTempFile("serialized_wrapper", "ser"); wrapper.toFile(ser); ApiWrapper other = ApiWrapper.fromFile(ser); @@ -452,7 +452,7 @@ public void shouldSetProxy() throws Exception { @Test @SuppressWarnings("serial") public void shouldHandleBrokenHttpClientNPE() throws Exception { final HttpClient client = mock(HttpClient.class); - ApiWrapper broken = new ApiWrapper("invalid", "invalid", URI.create("redirect://me"), null, Env.SANDBOX) { + ApiWrapper broken = new ApiWrapper("invalid", "invalid", URI.create("redirect://me"), null) { @Override public HttpClient getHttpClient() { return client; @@ -471,7 +471,7 @@ public HttpClient getHttpClient() { @Test @SuppressWarnings("serial") public void shouldHandleBrokenHttpClientIAE() throws Exception { final HttpClient client = mock(HttpClient.class); - ApiWrapper broken = new ApiWrapper("invalid", "invalid", URI.create("redirect://me"), null, Env.SANDBOX) { + ApiWrapper broken = new ApiWrapper("invalid", "invalid", URI.create("redirect://me"), null) { @Override public HttpClient getHttpClient() { return client; @@ -491,7 +491,7 @@ public HttpClient getHttpClient() { public void shouldSafeExecute() throws Exception { final HttpClient client = mock(HttpClient.class); - ApiWrapper broken = new ApiWrapper("invalid", "invalid", URI.create("redirect://me"), null, Env.SANDBOX) { + ApiWrapper broken = new ApiWrapper("invalid", "invalid", URI.create("redirect://me"), null) { @Override public HttpClient getHttpClient() { return client; diff --git a/src/test/java/com/soundcloud/api/CloudAPIIntegrationTest.java b/src/test/java/com/soundcloud/api/CloudAPIIntegrationTest.java index 78c61f2..0fea870 100644 --- a/src/test/java/com/soundcloud/api/CloudAPIIntegrationTest.java +++ b/src/test/java/com/soundcloud/api/CloudAPIIntegrationTest.java @@ -46,8 +46,7 @@ public void setUp() throws Exception { CLIENT_ID, CLIENT_SECRET, null, - null, - Env.SANDBOX); + null); } private Token login(String... scopes) throws IOException { From 5ee54cef07fe45c41cd94abe09fb2387e34b04d1 Mon Sep 17 00:00:00 2001 From: Jan Berkel Date: Mon, 5 Nov 2012 18:11:32 +0100 Subject: [PATCH 087/156] Prepare release --- CHANGES.md | 2 +- README.md | 4 ++-- build.gradle | 2 +- pom.xml | 2 +- src/main/java/com/soundcloud/api/CloudAPI.java | 2 +- src/main/java/com/soundcloud/api/package-info.java | 2 +- 6 files changed, 7 insertions(+), 7 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index d41491a..641dc43 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,4 +1,4 @@ -## 1.1.3-SNAPSHOT +## 1.2.0 2012-11-05 * Handle more broken HTTP client behaviour * Remove sandbox environment (it is no longer supported) diff --git a/README.md b/README.md index 3180fe4..ae742d2 100644 --- a/README.md +++ b/README.md @@ -256,7 +256,7 @@ See LICENSE for details. [Apache HttpClient]: http://hc.apache.org/httpcomponents-client-ga/ [HttpMime]: http://hc.apache.org/httpcomponents-client-ga/httpmime [json-java]: http://json.org/java/ -[javadoc]: http://soundcloud.github.com/java-api-wrapper/javadoc/1.1.2/com/soundcloud/api/package-summary.html +[javadoc]: http://soundcloud.github.com/java-api-wrapper/javadoc/1.2.0/com/soundcloud/api/package-summary.html [soundcloudapi-java]: http://code.google.com/p/soundcloudapi-java/ [soundcloudapi-java-annouce]: http://blog.soundcloud.com/2010/01/08/java-wrapper/ [CreateWrapper]: https://github.com/soundcloud/java-api-wrapper/blob/master/src/examples/java/com/soundcloud/api/examples/CreateWrapper.java @@ -268,7 +268,7 @@ See LICENSE for details. [SoundCloud Android]: https://market.android.com/details?id=com.soundcloud.android [register-app]: http://soundcloud.com/you/apps/new [Apache Maven]: http://maven.apache.org/ -[jar-all]: https://github.com/downloads/soundcloud/java-api-wrapper/java-api-wrapper-1.1.2-all.jar +[jar-all]: https://github.com/downloads/soundcloud/java-api-wrapper/java-api-wrapper-1.2.0-all.jar [downloads]: https://github.com/soundcloud/java-api-wrapper/archives/master [snapshots]: https://oss.sonatype.org/content/repositories/snapshots/com/soundcloud/java-api-wrapper/ [releases]: https://oss.sonatype.org/content/repositories/releases/com/soundcloud/java-api-wrapper/ diff --git a/build.gradle b/build.gradle index 9451586..0582b70 100644 --- a/build.gradle +++ b/build.gradle @@ -4,7 +4,7 @@ apply plugin: 'eclipse' apply plugin: 'maven' sourceCompatibility = 1.6 -version = '1.1.3-SNAPSHOT' +version = '1.2.0' group = 'com.soundcloud' repositories { mavenCentral() } diff --git a/pom.xml b/pom.xml index af3db84..9e1ddb9 100644 --- a/pom.xml +++ b/pom.xml @@ -3,7 +3,7 @@ 4.0.0 com.soundcloud java-api-wrapper - 1.1.3-SNAPSHOT + 1.2.0-SNAPSHOT SoundCloud Java API wrapper SoundCloud Java API wrapper (OAuth2 only), works on Android https://github.com/soundcloud/java-api-wrapper#readme diff --git a/src/main/java/com/soundcloud/api/CloudAPI.java b/src/main/java/com/soundcloud/api/CloudAPI.java index 7744271..0d21232 100644 --- a/src/main/java/com/soundcloud/api/CloudAPI.java +++ b/src/main/java/com/soundcloud/api/CloudAPI.java @@ -30,7 +30,7 @@ public interface CloudAPI { // other constants String REALM = "SoundCloud"; String OAUTH_SCHEME = "oauth"; - String VERSION = "1.1.2"; + String VERSION = "1.2.0"; String USER_AGENT = "SoundCloud Java Wrapper ("+VERSION+")"; diff --git a/src/main/java/com/soundcloud/api/package-info.java b/src/main/java/com/soundcloud/api/package-info.java index 4f24a48..942d2a9 100644 --- a/src/main/java/com/soundcloud/api/package-info.java +++ b/src/main/java/com/soundcloud/api/package-info.java @@ -7,6 +7,6 @@ * * @see com.soundcloud.api.ApiWrapper * @author Jan Berkel - * @version 1.1.2, 04/10/12 + * @version 1.2.0, 05/11/12 */ package com.soundcloud.api; From eb073044eef3c726e5b253e51d7e60d09fbdcdd8 Mon Sep 17 00:00:00 2001 From: Jan Berkel Date: Mon, 5 Nov 2012 18:13:13 +0100 Subject: [PATCH 088/156] [maven-release-plugin] prepare release 1.2.0 --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 9e1ddb9..bc122b6 100644 --- a/pom.xml +++ b/pom.xml @@ -3,7 +3,7 @@ 4.0.0 com.soundcloud java-api-wrapper - 1.2.0-SNAPSHOT + 1.2.0 SoundCloud Java API wrapper SoundCloud Java API wrapper (OAuth2 only), works on Android https://github.com/soundcloud/java-api-wrapper#readme From d83d5e0b2ffda8baf26e6994d6e81a37cd0d7433 Mon Sep 17 00:00:00 2001 From: Jan Berkel Date: Mon, 5 Nov 2012 18:13:21 +0100 Subject: [PATCH 089/156] [maven-release-plugin] prepare for next development iteration --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index bc122b6..ebf1a4a 100644 --- a/pom.xml +++ b/pom.xml @@ -3,7 +3,7 @@ 4.0.0 com.soundcloud java-api-wrapper - 1.2.0 + 1.2.1-SNAPSHOT SoundCloud Java API wrapper SoundCloud Java API wrapper (OAuth2 only), works on Android https://github.com/soundcloud/java-api-wrapper#readme From 0109b27464aa98517dedba9879fa656bb0810716 Mon Sep 17 00:00:00 2001 From: Jan Berkel Date: Mon, 5 Nov 2012 18:28:00 +0100 Subject: [PATCH 090/156] Remove live reference --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index ae742d2..2238c25 100644 --- a/README.md +++ b/README.md @@ -20,7 +20,7 @@ Create a wrapper instance: ```java ApiWrapper wrapper = new ApiWrapper("client_id", "client_secret", - null, null, Env.LIVE); + null, null); ``` Obtain a token: From eb7e4cc6d9672afb874aaff494558a39a2517ff5 Mon Sep 17 00:00:00 2001 From: Jan Berkel Date: Mon, 5 Nov 2012 18:32:55 +0100 Subject: [PATCH 091/156] Remove env references --- README.md | 3 +-- build.gradle | 2 +- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 2238c25..c98e0c1 100644 --- a/README.md +++ b/README.md @@ -137,8 +137,7 @@ First create a wrapper and remember to substitute all credentials with real ones $ gradle createWrapper -Pclient_id=my_client_id \ -Pclient_secret=mys3cr3t \ -Plogin=api-testing \ - -Ppassword=testing \ - -Penv=live + -Ppassword=testing # with plain java $ java -classpath java-api-wrapper-1.x.y-all.jar \ diff --git a/build.gradle b/build.gradle index 0582b70..d6adb54 100644 --- a/build.gradle +++ b/build.gradle @@ -76,7 +76,7 @@ def example(name, mainClass, arguments) { } } } -example('createWrapper', 'CreateWrapper', { [client_id, client_secret, login, password, env] }) +example('createWrapper', 'CreateWrapper', { [client_id, client_secret, login, password] }) example('facebookLogin', 'FacebookLogin', { [] }) example('getResource', 'GetResource', { resource }) example('putResource', 'PutResource', { [resource, content, contentType] }) From 4045da573f245aa54316516ad095874b90c5a906 Mon Sep 17 00:00:00 2001 From: Jan Berkel Date: Mon, 5 Nov 2012 18:36:25 +0100 Subject: [PATCH 092/156] Formatting --- README.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/README.md b/README.md index c98e0c1..754b834 100644 --- a/README.md +++ b/README.md @@ -19,8 +19,7 @@ app and makes use of Android's [intent][] framework. Create a wrapper instance: ```java -ApiWrapper wrapper = new ApiWrapper("client_id", "client_secret", - null, null); +ApiWrapper wrapper = new ApiWrapper("client_id", "client_secret", null, null); ``` Obtain a token: From f38e1a98d959bcdf5ca2d0577e4fb7d0883b2362 Mon Sep 17 00:00:00 2001 From: Jan Berkel Date: Sun, 6 Jan 2013 22:59:14 +0100 Subject: [PATCH 093/156] Fix JSON token parsing Closes #8 --- src/main/java/com/soundcloud/api/Token.java | 13 +++++++------ src/test/java/com/soundcloud/api/TokenTest.java | 15 +++++++++++++++ 2 files changed, 22 insertions(+), 6 deletions(-) diff --git a/src/main/java/com/soundcloud/api/Token.java b/src/main/java/com/soundcloud/api/Token.java index f61dab8..92a5882 100644 --- a/src/main/java/com/soundcloud/api/Token.java +++ b/src/main/java/com/soundcloud/api/Token.java @@ -60,18 +60,19 @@ public Token(String access, String refresh, String scope) { public Token(JSONObject json) throws IOException { try { for (Iterator it = json.keys(); it.hasNext(); ) { - String key = it.next().toString(); + final String key = it.next().toString(); if (ACCESS_TOKEN.equals(key)) { - access = json.getString(ACCESS_TOKEN); + access = json.getString(key); } else if (REFRESH_TOKEN.equals(key)) { // refresh token won't be set if we don't expire - refresh = json.getString(REFRESH_TOKEN); - expiresIn = System.currentTimeMillis() + json.getLong(EXPIRES_IN) * 1000; + refresh = json.getString(key); + } else if (EXPIRES_IN.equals(key)) { + expiresIn = System.currentTimeMillis() + json.getLong(key) * 1000; } else if (SCOPE.equals(key)) { - scope = json.getString(SCOPE); + scope = json.getString(key); } else { // custom parameter - customParameters.put(key, json.getString(key)); + customParameters.put(key, json.get(key).toString()); } } } catch (JSONException e) { diff --git a/src/test/java/com/soundcloud/api/TokenTest.java b/src/test/java/com/soundcloud/api/TokenTest.java index f31b82f..49a9b07 100644 --- a/src/test/java/com/soundcloud/api/TokenTest.java +++ b/src/test/java/com/soundcloud/api/TokenTest.java @@ -82,6 +82,21 @@ public void shouldParseJsonResponse() throws Exception { assertNotNull(t.getExpiresIn()); } + @Test + public void shouldParseJsonResponseDifferentKeyOrder() throws Exception { + Token t = new Token(new JSONObject("{\n" + + " \"expires_in\": 3600,\n" + + " \"access_token\": \"1234\",\n" + + " \"scope\": \"*\",\n" + + " \"refresh_token\": \"5678\"\n" + + "}")); + + assertThat(t.scoped("*"), is(true)); + assertThat(t.access, equalTo("1234")); + assertThat(t.refresh, equalTo("5678")); + assertNotNull(t.getExpiresIn()); + } + @Test public void shouldParseJsonWithCustomParameters() throws Exception { Token t = new Token(new JSONObject("{\n" + From 0cdef06d6bf7ba939656008af6bb35f6c48ec0c4 Mon Sep 17 00:00:00 2001 From: Jan Berkel Date: Sun, 6 Jan 2013 23:05:00 +0100 Subject: [PATCH 094/156] fixed link: market -> playstore --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 754b834..485c085 100644 --- a/README.md +++ b/README.md @@ -263,7 +263,7 @@ See LICENSE for details. [PostResource]: https://github.com/soundcloud/java-api-wrapper/blob/master/src/examples/java/com/soundcloud/api/examples/PostResource.java [UploadFile]: https://github.com/soundcloud/java-api-wrapper/blob/master/src/examples/java/com/soundcloud/api/examples/UploadFile.java [FacebookConnect]: https://github.com/soundcloud/java-api-wrapper/blob/master/src/examples/java/com/soundcloud/api/examples/FacebookConnect.java -[SoundCloud Android]: https://market.android.com/details?id=com.soundcloud.android +[SoundCloud Android]: https://play.google.com/store/apps/details?id=com.soundcloud.android [register-app]: http://soundcloud.com/you/apps/new [Apache Maven]: http://maven.apache.org/ [jar-all]: https://github.com/downloads/soundcloud/java-api-wrapper/java-api-wrapper-1.2.0-all.jar From ba9f50571fe114f35202d32384fafa381925d448 Mon Sep 17 00:00:00 2001 From: Matthias Kaeppler Date: Thu, 10 Jan 2013 16:56:01 +0100 Subject: [PATCH 095/156] Add ApiResponseException. Clients can use this to check the HTTP status code for custom error handling. --- .../java/com/soundcloud/api/ApiWrapper.java | 26 +++++++++--------- .../java/com/soundcloud/api/CloudAPI.java | 27 ++++++++++++++++--- .../com/soundcloud/api/ApiWrapperTest.java | 10 +++---- 3 files changed, 42 insertions(+), 21 deletions(-) diff --git a/src/main/java/com/soundcloud/api/ApiWrapper.java b/src/main/java/com/soundcloud/api/ApiWrapper.java index 3d1a052..db1d52f 100644 --- a/src/main/java/com/soundcloud/api/ApiWrapper.java +++ b/src/main/java/com/soundcloud/api/ApiWrapper.java @@ -263,21 +263,23 @@ protected Token requestToken(Request request) throws IOException { HttpResponse response = safeExecute(env.sslResourceHost, request.buildRequest(HttpPost.class)); final int status = response.getStatusLine().getStatusCode(); - if (status == HttpStatus.SC_OK) { - final Token token = new Token(Http.getJSON(response)); - if (listener != null) listener.onTokenRefreshed(token); - return token; - } else { - String error = ""; - try { + String error = null; + try { + if (status == HttpStatus.SC_OK) { + final Token token = new Token(Http.getJSON(response)); + if (listener != null) listener.onTokenRefreshed(token); + return token; + } else { error = Http.getJSON(response).getString("error"); - } catch (IOException ignored) { - } catch (JSONException ignored) { } - throw status == HttpStatus.SC_UNAUTHORIZED ? - new InvalidTokenException(status, error) : - new IOException(status+" "+response.getStatusLine().getReasonPhrase()+" "+error); + } catch (IOException ignored) { + error = ignored.getMessage(); + } catch (JSONException ignored) { + error = ignored.getMessage(); } + throw status == HttpStatus.SC_UNAUTHORIZED ? + new InvalidTokenException(status, error) : + new ApiResponseException(response, error); } /** diff --git a/src/main/java/com/soundcloud/api/CloudAPI.java b/src/main/java/com/soundcloud/api/CloudAPI.java index 0d21232..f381183 100644 --- a/src/main/java/com/soundcloud/api/CloudAPI.java +++ b/src/main/java/com/soundcloud/api/CloudAPI.java @@ -261,16 +261,35 @@ public InvalidTokenException(int code, String status) { } } - class ResolverException extends IOException { + /** + * Thrown if resolving the audio stream of a SoundCloud sound fails. + */ + class ResolverException extends ApiResponseException { + + public ResolverException(String s, HttpResponse resp) { + super(resp, s); + } + + public ResolverException(Throwable throwable, HttpResponse response) { + super(throwable, response); + } + } + + /** + * Thrown if the service API responds in error. The HTTP status code can be obtained via {@link #getStatusCode()}. + */ + class ApiResponseException extends IOException { private static final long serialVersionUID = -2990651725862868387L; public final HttpResponse response; - public ResolverException(String s, HttpResponse resp) { - super(s); + + public ApiResponseException(HttpResponse resp, String error) { + super(resp.getStatusLine().getStatusCode() + ": [" + resp.getStatusLine().getReasonPhrase() + "] " + + (error != null ? error : "")); this.response = resp; } - public ResolverException(Throwable throwable, HttpResponse response) { + public ApiResponseException(Throwable throwable, HttpResponse response) { super(throwable == null ? null : throwable.toString()); initCause(throwable); this.response = response; diff --git a/src/test/java/com/soundcloud/api/ApiWrapperTest.java b/src/test/java/com/soundcloud/api/ApiWrapperTest.java index b2888f3..e4d3dae 100644 --- a/src/test/java/com/soundcloud/api/ApiWrapperTest.java +++ b/src/test/java/com/soundcloud/api/ApiWrapperTest.java @@ -191,8 +191,8 @@ public void shouldGetTokensWhenLoggingInViaAuthorizationCodeAndScope() throws Ex assertNull(t.getExpiresIn()); } - @Test(expected = IOException.class) - public void shouldThrowIOExceptionWhenLoginFailed() throws Exception { + @Test(expected = CloudAPI.InvalidTokenException.class) + public void shouldThrowInvalidTokenExceptionWhenLoginFailed() throws Exception { layer.addPendingHttpResponse(401, "{\n" + " \"error\": \"Error!\"\n" + "}"); @@ -200,8 +200,8 @@ public void shouldThrowIOExceptionWhenLoginFailed() throws Exception { } - @Test(expected = IOException.class) - public void shouldThrowIOExceptionWhenInvalidJSONReturned() throws Exception { + @Test(expected = CloudAPI.ApiResponseException.class) + public void shouldThrowApiResponseExceptionWhenInvalidJSONReturned() throws Exception { layer.addPendingHttpResponse(200, "I'm invalid JSON!"); api.login("foo", "bar"); } @@ -212,7 +212,7 @@ public void shouldContainInvalidJSONInExceptionMessage() throws Exception { try { api.login("foo", "bar"); fail("expected IOException"); - } catch (IOException e) { + } catch (CloudAPI.ApiResponseException e) { assertThat(e.getMessage(), containsString("I'm invalid JSON!")); } } From 7d11555bbb7f89ab624b52b1bb4b41890eb61a44 Mon Sep 17 00:00:00 2001 From: Jan Berkel Date: Thu, 10 Jan 2013 19:13:50 +0100 Subject: [PATCH 096/156] Added javadoc --- src/main/java/com/soundcloud/api/ApiWrapper.java | 1 + 1 file changed, 1 insertion(+) diff --git a/src/main/java/com/soundcloud/api/ApiWrapper.java b/src/main/java/com/soundcloud/api/ApiWrapper.java index db1d52f..d5c7108 100644 --- a/src/main/java/com/soundcloud/api/ApiWrapper.java +++ b/src/main/java/com/soundcloud/api/ApiWrapper.java @@ -258,6 +258,7 @@ public String getUserAgent() { * @return the token * @throws java.io.IOException network error * @throws com.soundcloud.api.CloudAPI.InvalidTokenException unauthorized + * @throws com.soundcloud.api.CloudAPI.ApiResponseException http error */ protected Token requestToken(Request request) throws IOException { HttpResponse response = safeExecute(env.sslResourceHost, request.buildRequest(HttpPost.class)); From 63c595172d9bb555ce62f17d862a952aa764dec0 Mon Sep 17 00:00:00 2001 From: Jan Berkel Date: Thu, 10 Jan 2013 19:16:24 +0100 Subject: [PATCH 097/156] Re-enable + fix test for parameter parsing --- src/main/java/com/soundcloud/api/Request.java | 54 ++++++++++++++++--- .../java/com/soundcloud/api/RequestTest.java | 2 +- 2 files changed, 48 insertions(+), 8 deletions(-) diff --git a/src/main/java/com/soundcloud/api/Request.java b/src/main/java/com/soundcloud/api/Request.java index cd317ac..64b2f45 100644 --- a/src/main/java/com/soundcloud/api/Request.java +++ b/src/main/java/com/soundcloud/api/Request.java @@ -4,7 +4,6 @@ import org.apache.http.NameValuePair; import org.apache.http.client.methods.HttpEntityEnclosingRequestBase; import org.apache.http.client.methods.HttpRequestBase; -import org.apache.http.client.utils.URLEncodedUtils; import org.apache.http.entity.StringEntity; import org.apache.http.entity.mime.HttpMultipartMode; import org.apache.http.entity.mime.MIME; @@ -14,6 +13,7 @@ import org.apache.http.entity.mime.content.FileBody; import org.apache.http.entity.mime.content.StringBody; import org.apache.http.message.BasicNameValuePair; +import org.apache.http.protocol.HTTP; import org.apache.james.mime4j.util.CharsetUtil; import java.io.File; @@ -22,6 +22,7 @@ import java.io.UnsupportedEncodingException; import java.net.URI; import java.net.URLDecoder; +import java.net.URLEncoder; import java.nio.ByteBuffer; import java.nio.charset.Charset; import java.util.ArrayList; @@ -77,12 +78,17 @@ public Request(String resource) { resource.length()); for (String s : query.split("&")) { String[] kv = s.split("=", 2); - if (kv != null && kv.length == 2) { + if (kv != null) { try { - mParams.add(new BasicNameValuePair( - URLDecoder.decode(kv[0], "UTF-8"), - URLDecoder.decode(kv[1], "UTF-8"))); - } catch (UnsupportedEncodingException ignored) {} + if (kv.length == 2) { + mParams.add(new BasicNameValuePair( + URLDecoder.decode(kv[0], "UTF-8"), + URLDecoder.decode(kv[1], "UTF-8"))); + } else if (kv.length == 1) { + mParams.add(new BasicNameValuePair(URLDecoder.decode(kv[0], "UTF-8"), null)); + } + } catch (UnsupportedEncodingException ignored) { + } } } mResource = resource.substring(0, resource.indexOf("?")); @@ -208,7 +214,7 @@ public int size() { * list of parameters in an HTTP PUT or HTTP POST. */ public String queryString() { - return URLEncodedUtils.format(mParams, "UTF-8"); + return format(mParams, "UTF-8"); } /** @@ -562,4 +568,38 @@ public String getFilename() { } } } + + /** + * Returns a String that is suitable for use as an application/x-www-form-urlencoded + * list of parameters in an HTTP PUT or HTTP POST. + * + * @param parameters The parameters to include. + * @param encoding The encoding to use. + */ + public static String format( + final List parameters, + final String encoding) { + final StringBuilder result = new StringBuilder(); + for (final NameValuePair parameter : parameters) { + final String encodedName = encode(parameter.getName(), encoding); + final String value = parameter.getValue(); + final String encodedValue = value != null ? encode(value, encoding) : ""; + if (result.length() > 0) + result.append("&"); + result.append(encodedName); + if (value != null) { + result.append("="); + result.append(encodedValue); + } + } + return result.toString(); + } + + private static String encode(final String content, final String encoding) { + try { + return URLEncoder.encode(content, encoding != null ? encoding : HTTP.DEFAULT_CONTENT_CHARSET); + } catch (UnsupportedEncodingException problem) { + throw new IllegalArgumentException(problem); + } + } } diff --git a/src/test/java/com/soundcloud/api/RequestTest.java b/src/test/java/com/soundcloud/api/RequestTest.java index f30a0f7..06e1275 100644 --- a/src/test/java/com/soundcloud/api/RequestTest.java +++ b/src/test/java/com/soundcloud/api/RequestTest.java @@ -47,7 +47,7 @@ public void shouldGenerateUrlWithParameters() throws Exception { } - @Test @Ignore + @Test public void shouldNotModifyOriginalRequest() throws Exception { String url = "http://ec-media.soundcloud.com/SdPniMt7cZzj.128.mp3?ff61182e3c2ecefa438cd02102d0e385713f0c1f" + "af3b0339595660fd0603ed1dd95c308fdf4dfe37b272d5fc302cd60875f62fda2557f961990ca6e770fdb81c291f729" + From 28c4ef8b2624e3e06a40e0859526439e7b9def5e Mon Sep 17 00:00:00 2001 From: Jan Berkel Date: Thu, 10 Jan 2013 19:18:03 +0100 Subject: [PATCH 098/156] Re-enable integration tests --- .../api/CloudAPIIntegrationTest.java | 63 ++++++++++++------- 1 file changed, 40 insertions(+), 23 deletions(-) diff --git a/src/test/java/com/soundcloud/api/CloudAPIIntegrationTest.java b/src/test/java/com/soundcloud/api/CloudAPIIntegrationTest.java index 0fea870..f3d3e83 100644 --- a/src/test/java/com/soundcloud/api/CloudAPIIntegrationTest.java +++ b/src/test/java/com/soundcloud/api/CloudAPIIntegrationTest.java @@ -23,15 +23,21 @@ import java.util.concurrent.BrokenBarrierException; import java.util.concurrent.CyclicBarrier; -@Ignore // sandbox is kaputt public class CloudAPIIntegrationTest implements Params.Track, Endpoints { - // http://sandbox-soundcloud.com/you/apps/java-api-wrapper-test-app + // https://soundcloud.com/you/apps/java-api-wrapper // user: api-testing - static final String CLIENT_ID = "yH1Jv2C5fhIbZfGTpKtujQ"; - static final String CLIENT_SECRET = "C6o8jc517b6PIw0RKtcfQsbOK3BjGpxWFLg977UiguY"; + static final String CLIENT_ID = "40d3111c6b4d02096c6ce35fdf90bf58"; + static final String CLIENT_SECRET = "ff3685dbf02ce789a16631b0028e0512"; + + public static final String TRACK_PERMALINK = "http://soundcloud.com/jberkel/nobody-home"; + public static final long USER_ID = 18173653L; + public static final long TRACK_LENGTH = 224861L; CloudAPI api; + static final String USERNAME = "android-testing"; + static final String PASSWORD = "android-testing"; + /* To get full HTTP logging, add the following system properties: -Dorg.apache.commons.logging.Log=org.apache.commons.logging.impl.SimpleLog @@ -50,7 +56,7 @@ public void setUp() throws Exception { } private Token login(String... scopes) throws IOException { - return api.login("api-testing", "testing", scopes); + return api.login(USERNAME, PASSWORD, scopes); } @Test @@ -97,7 +103,7 @@ public void shouldNotGetASignupTokenWhenInofficialApp() throws Exception { public void shouldGetATokenUsingExtensionGrantTypes() throws Exception { // TODO String fbToken = "fbToken"; - Token token = api.extensionGrantType(CloudAPI.FACEBOOK_GRANT_TYPE +fbToken); + api.extensionGrantType(CloudAPI.FACEBOOK_GRANT_TYPE +fbToken); } @Test @@ -137,11 +143,11 @@ public void shouldRefreshAutomaticallyWhenTokenExpired() throws Exception { public void shouldResolveUrls() throws Exception { login(); - long id = api.resolve("http://sandbox-soundcloud.com/api-testing"); - assertThat(id, is(1862213L)); + long id = api.resolve("http://soundcloud.com/" + USERNAME); + assertThat(id, is(USER_ID)); try { - id = api.resolve("http://sandbox-soundcloud.com/i-do-no-exist"); + id = api.resolve("http://soundcloud.com/i-do-no-exist-no-no-no"); fail("expected resolver exception, got: "+id); } catch (CloudAPI.ResolverException e) { // expected @@ -152,13 +158,15 @@ public void shouldResolveUrls() throws Exception { @Test public void shouldResolveStreamUrls() throws Exception { login(); - Stream resolved = api.resolveStreamUrl("https://api.sandbox-soundcloud.com/tracks/2112881/stream", false); - assertThat(resolved.url, equalTo("https://api.sandbox-soundcloud.com/tracks/2112881/stream")); - assertThat(resolved.streamUrl, containsString("http://ak-media.soundcloud.com/")); + String streamUrl = getApiUrlFromPermalink(TRACK_PERMALINK) + "/stream"; + Stream resolved = api.resolveStreamUrl(streamUrl, false); + + assertThat(resolved.url, equalTo(streamUrl)); + assertThat(resolved.streamUrl, containsString("http://ec-media.soundcloud.com/")); assertTrue("expire should be in the future", resolved.expires > System.currentTimeMillis()); - assertThat(resolved.eTag, equalTo("\"a1782cf9976c2bc26988929e956def26\"")); + assertThat(resolved.eTag, equalTo("\"980f61d6d6ee26ffe0c78aef618d786f\"")); } @Test @Ignore /* playcounts not deployed on sandbox */ @@ -166,16 +174,18 @@ public void shouldResolveStreamUrlAndSkipPlaycountLogging() throws Exception { // need the playcount scope for this to work assertTrue(login(Token.SCOPE_PLAYCOUNT).scoped(Token.SCOPE_PLAYCOUNT)); - int count = Http.getJSON(api.get(Request.to("/tracks/2100832"))).getInt("playback_count"); - api.resolveStreamUrl("https://api.sandbox-soundcloud.com/tracks/2100832/stream", false); - int count2 = Http.getJSON(api.get(Request.to("/tracks/2100832"))).getInt("playback_count"); + long trackId = api.resolve(TRACK_PERMALINK); + + int count = Http.getJSON(api.get(Request.to("/tracks/"+trackId))).getInt("playback_count"); + api.resolveStreamUrl("https://api.soundcloud.com/tracks/"+trackId+"/stream", false); + int count2 = Http.getJSON(api.get(Request.to("/tracks/"+trackId))).getInt("playback_count"); assertTrue(String.format("%d !> %d", count2, count), count2 > count); // resolve again, this time skipping count - api.resolveStreamUrl("https://api.sandbox-soundcloud.com/tracks/2100832/stream", true); + api.resolveStreamUrl("https://api.soundcloud.com/tracks/"+trackId+"/stream", true); - int count3 = Http.getJSON(api.get(Request.to("/tracks/2100832"))).getInt("playback_count"); + int count3 = Http.getJSON(api.get(Request.to("/tracks/"+trackId))).getInt("playback_count"); assertTrue(String.format("%d != %d", count3, count2), count3 == count2); } @@ -183,7 +193,7 @@ public void shouldResolveStreamUrlAndSkipPlaycountLogging() throws Exception { public void shouldThrowResolverExceptionWhenStreamCannotBeResolved() throws Exception { login(); try { - Stream s = api.resolveStreamUrl("https://api.sandbox-soundcloud.com/tracks/999919191/stream", false); + Stream s = api.resolveStreamUrl("https://api.soundcloud.com/tracks/999919191/stream", false); fail("expected resolver exception, got: "+s); } catch (CloudAPI.ResolverException e) { // expected @@ -195,8 +205,10 @@ public void shouldThrowResolverExceptionWhenStreamCannotBeResolved() throws Exce public void shouldSupportRangeRequest() throws Exception { login(); - Stream resolved = api.resolveStreamUrl("https://api.sandbox-soundcloud.com/tracks/2112881/stream", false); - assertThat(resolved.contentLength, is(19643L)); + String streamUrl = getApiUrlFromPermalink(TRACK_PERMALINK)+"/stream"; + + Stream resolved = api.resolveStreamUrl(streamUrl, false); + assertThat(resolved.contentLength, is(TRACK_LENGTH)); HttpResponse resp = api .getHttpClient() @@ -205,7 +217,7 @@ public void shouldSupportRangeRequest() throws Exception { assertThat(resp.getStatusLine().toString(), resp.getStatusLine().getStatusCode(), is(206)); Header range = resp.getFirstHeader("Content-Range"); assertThat(range, notNullValue()); - assertThat(range.getValue(), equalTo("bytes 50-100/19643")); + assertThat(range.getValue(), equalTo("bytes 50-100/"+TRACK_LENGTH)); assertThat(resp.getEntity().getContentLength(), is(51L)); } @@ -222,7 +234,7 @@ public void readMyDetails() throws Exception { JSONObject me = Http.getJSON(resp); - assertThat(me.getString("username"), equalTo("api-testing")); + assertThat(me.getString("username"), equalTo(USERNAME)); // writeResponse(resp, "me.json"); } @@ -346,4 +358,9 @@ private void writeResponse(HttpResponse resp, String file) throws IOException { is.close(); fos.close(); } + + private String getApiUrlFromPermalink(String permalink) throws IOException { + long trackId = api.resolve(permalink); + return "https://api.soundcloud.com/tracks/" + trackId; + } } From 462d4e2e817aa3f2e9e9b9d1ad7874624aab3e6f Mon Sep 17 00:00:00 2001 From: Jan Berkel Date: Thu, 10 Jan 2013 19:20:43 +0100 Subject: [PATCH 099/156] Remove reference to CharsetUtil.getCharset Closes #7 --- src/main/java/com/soundcloud/api/Request.java | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/main/java/com/soundcloud/api/Request.java b/src/main/java/com/soundcloud/api/Request.java index 64b2f45..7d80bbd 100644 --- a/src/main/java/com/soundcloud/api/Request.java +++ b/src/main/java/com/soundcloud/api/Request.java @@ -14,7 +14,6 @@ import org.apache.http.entity.mime.content.StringBody; import org.apache.http.message.BasicNameValuePair; import org.apache.http.protocol.HTTP; -import org.apache.james.mime4j.util.CharsetUtil; import java.io.File; import java.io.IOException; @@ -376,7 +375,7 @@ public T buildRequest(Class method) { HttpEntityEnclosingRequestBase enclosingRequest = (HttpEntityEnclosingRequestBase) request; - final Charset charSet = CharsetUtil.getCharset("UTF-8"); + final Charset charSet = java.nio.charset.Charset.forName("UTF-8"); if (isMultipart()) { MultipartEntity multiPart = new MultipartEntity( HttpMultipartMode.BROWSER_COMPATIBLE, // XXX change this to STRICT once rack on server is upgraded From 8995c610c31912d1c053265e11ee6acae850b0bf Mon Sep 17 00:00:00 2001 From: Jan Berkel Date: Thu, 10 Jan 2013 19:54:01 +0100 Subject: [PATCH 100/156] Allow requests without login Closes #6 --- src/main/java/com/soundcloud/api/ApiWrapper.java | 12 ++++++++---- .../com/soundcloud/api/CloudAPIIntegrationTest.java | 7 +++++++ 2 files changed, 15 insertions(+), 4 deletions(-) diff --git a/src/main/java/com/soundcloud/api/ApiWrapper.java b/src/main/java/com/soundcloud/api/ApiWrapper.java index d5c7108..93ba560 100644 --- a/src/main/java/com/soundcloud/api/ApiWrapper.java +++ b/src/main/java/com/soundcloud/api/ApiWrapper.java @@ -82,9 +82,11 @@ * @see Using the SoundCloud API */ public class ApiWrapper implements CloudAPI, Serializable { - private static final long serialVersionUID = 3662083416905771921L; public static final String DEFAULT_CONTENT_TYPE = "application/json"; + private static final long serialVersionUID = 3662083416905771921L; + private static final Token EMPTY_TOKEN = new Token(null, null); + /** The current environment, only live possible for now */ public final Env env = Env.LIVE; @@ -124,7 +126,7 @@ public ApiWrapper(String clientId, mClientId = clientId; mClientSecret = clientSecret; mRedirectUri = redirectUri; - mToken = token == null ? new Token(null, null) : token; + mToken = token == null ? EMPTY_TOKEN : token; } @Override public Token login(String username, String password, String... scopes) throws IOException { @@ -519,7 +521,7 @@ public HttpResponse head(Request request) throws IOException { } @Override public void setToken(Token newToken) { - mToken = newToken; + mToken = newToken == null ? EMPTY_TOKEN : newToken; } @Override @@ -660,7 +662,9 @@ public static Header createOAuthHeader(Token token) { /** Adds an OAuth2 header to a given request */ protected HttpUriRequest addAuthHeader(HttpUriRequest request) { if (!request.containsHeader(AUTH.WWW_AUTH_RESP)) { - request.addHeader(createOAuthHeader(getToken())); + if (mToken != EMPTY_TOKEN) { + request.addHeader(createOAuthHeader(mToken)); + } } return request; } diff --git a/src/test/java/com/soundcloud/api/CloudAPIIntegrationTest.java b/src/test/java/com/soundcloud/api/CloudAPIIntegrationTest.java index f3d3e83..b120869 100644 --- a/src/test/java/com/soundcloud/api/CloudAPIIntegrationTest.java +++ b/src/test/java/com/soundcloud/api/CloudAPIIntegrationTest.java @@ -1,5 +1,6 @@ package com.soundcloud.api; +import static junit.framework.Assert.assertEquals; import static junit.framework.Assert.assertNotNull; import static junit.framework.Assert.assertTrue; import static org.hamcrest.CoreMatchers.*; @@ -59,6 +60,12 @@ private Token login(String... scopes) throws IOException { return api.login(USERNAME, PASSWORD, scopes); } + @Test + public void shouldBeAbleToMakePublicRequests() throws Exception { + HttpResponse response = api.get(Request.to("/tracks").with("client_id", CLIENT_ID, "order", "hotness")); + assertEquals(200, response.getStatusLine().getStatusCode()); + } + @Test public void shouldUploadASimpleAudioFile() throws Exception { login(); From 39f36a797e11d814b5fe93b9bf6f8e587084acc2 Mon Sep 17 00:00:00 2001 From: Jan Berkel Date: Thu, 10 Jan 2013 19:58:33 +0100 Subject: [PATCH 101/156] Update changelog, bump version --- CHANGES.md | 12 ++++++++++++ build.gradle | 2 +- src/main/java/com/soundcloud/api/CloudAPI.java | 2 +- src/main/java/com/soundcloud/api/package-info.java | 2 +- 4 files changed, 15 insertions(+), 3 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 641dc43..20224b9 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,3 +1,10 @@ +## 1.2.1 2013-01-10 + + * Added ApiResponseException to wrap HTTP error codes when logging in + * Fixed: Token.java: JSONObject["expires_in"] not a string [8] + * Fixed: NoSuchMethodError on CharsetUtil.getCharset() [7] + * Fixed: impossible to access a public resource without being logged [6] + ## 1.2.0 2012-11-05 * Handle more broken HTTP client behaviour @@ -42,3 +49,8 @@ ## 1.0.0 2011-05-19 * Initial release + + +[6]: https://github.com/soundcloud/java-api-wrapper/issues/6 +[7]: https://github.com/soundcloud/java-api-wrapper/issues/7 +[8]: https://github.com/soundcloud/java-api-wrapper/issues/8 diff --git a/build.gradle b/build.gradle index d6adb54..3e8987a 100644 --- a/build.gradle +++ b/build.gradle @@ -4,7 +4,7 @@ apply plugin: 'eclipse' apply plugin: 'maven' sourceCompatibility = 1.6 -version = '1.2.0' +version = '1.2.1' group = 'com.soundcloud' repositories { mavenCentral() } diff --git a/src/main/java/com/soundcloud/api/CloudAPI.java b/src/main/java/com/soundcloud/api/CloudAPI.java index f381183..4778791 100644 --- a/src/main/java/com/soundcloud/api/CloudAPI.java +++ b/src/main/java/com/soundcloud/api/CloudAPI.java @@ -30,7 +30,7 @@ public interface CloudAPI { // other constants String REALM = "SoundCloud"; String OAUTH_SCHEME = "oauth"; - String VERSION = "1.2.0"; + String VERSION = "1.2.1"; String USER_AGENT = "SoundCloud Java Wrapper ("+VERSION+")"; diff --git a/src/main/java/com/soundcloud/api/package-info.java b/src/main/java/com/soundcloud/api/package-info.java index 942d2a9..0db32d4 100644 --- a/src/main/java/com/soundcloud/api/package-info.java +++ b/src/main/java/com/soundcloud/api/package-info.java @@ -7,6 +7,6 @@ * * @see com.soundcloud.api.ApiWrapper * @author Jan Berkel - * @version 1.2.0, 05/11/12 + * @version 1.2.1, 10/01/12 */ package com.soundcloud.api; From 7ac79bd0c3dad90e8d0ca83549be714f73334cab Mon Sep 17 00:00:00 2001 From: Jan Berkel Date: Thu, 10 Jan 2013 20:00:45 +0100 Subject: [PATCH 102/156] Update pom --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index ebf1a4a..ca09ab7 100644 --- a/pom.xml +++ b/pom.xml @@ -1,5 +1,5 @@ - + 4.0.0 com.soundcloud java-api-wrapper From db246f622ec5249d2d58b40ff0e98ac3e252ce3e Mon Sep 17 00:00:00 2001 From: Jan Berkel Date: Thu, 10 Jan 2013 20:04:11 +0100 Subject: [PATCH 103/156] [maven-release-plugin] prepare release 1.2.1 --- pom.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pom.xml b/pom.xml index ca09ab7..b9c95b1 100644 --- a/pom.xml +++ b/pom.xml @@ -1,9 +1,9 @@ - + 4.0.0 com.soundcloud java-api-wrapper - 1.2.1-SNAPSHOT + 1.2.1 SoundCloud Java API wrapper SoundCloud Java API wrapper (OAuth2 only), works on Android https://github.com/soundcloud/java-api-wrapper#readme From fdbe690b2245a4843d8cb1d383587e4a2c58fb15 Mon Sep 17 00:00:00 2001 From: Jan Berkel Date: Thu, 10 Jan 2013 20:04:23 +0100 Subject: [PATCH 104/156] [maven-release-plugin] prepare for next development iteration --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index b9c95b1..29c6fd9 100644 --- a/pom.xml +++ b/pom.xml @@ -3,7 +3,7 @@ 4.0.0 com.soundcloud java-api-wrapper - 1.2.1 + 1.2.2-SNAPSHOT SoundCloud Java API wrapper SoundCloud Java API wrapper (OAuth2 only), works on Android https://github.com/soundcloud/java-api-wrapper#readme From 07869e00064cc2785d9408003f5e3f7e4fe4e4d0 Mon Sep 17 00:00:00 2001 From: Jan Berkel Date: Sun, 13 Jan 2013 01:25:34 +0100 Subject: [PATCH 105/156] trigger build From 02eccfc43fd42a7b9ffa5d307ce3902ed48edfa8 Mon Sep 17 00:00:00 2001 From: Jon Schmidt Date: Sun, 3 Feb 2013 13:39:56 +0100 Subject: [PATCH 106/156] format locale, fixes non-english request formatting issue --- src/main/java/com/soundcloud/api/Request.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/main/java/com/soundcloud/api/Request.java b/src/main/java/com/soundcloud/api/Request.java index 7d80bbd..697beff 100644 --- a/src/main/java/com/soundcloud/api/Request.java +++ b/src/main/java/com/soundcloud/api/Request.java @@ -28,6 +28,7 @@ import java.util.HashMap; import java.util.Iterator; import java.util.List; +import java.util.Locale; import java.util.Map; /** @@ -127,7 +128,7 @@ public Request(Request request) { public static Request to(String resource, Object... args) { if (args != null && args.length > 0) { - resource = String.format(resource, args); + resource = String.format(Locale.ENGLISH, resource, args); } return new Request(resource); } From 8ba5a184e25c27552a5423e4479301151172bb26 Mon Sep 17 00:00:00 2001 From: jonschmidt Date: Mon, 4 Feb 2013 10:41:33 +0100 Subject: [PATCH 107/156] added basic playlist endpoints --- src/main/java/com/soundcloud/api/Endpoints.java | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/main/java/com/soundcloud/api/Endpoints.java b/src/main/java/com/soundcloud/api/Endpoints.java index cad051f..362dfe2 100644 --- a/src/main/java/com/soundcloud/api/Endpoints.java +++ b/src/main/java/com/soundcloud/api/Endpoints.java @@ -16,6 +16,10 @@ public interface Endpoints { String TRACK_PLAYS = "/tracks/%d/plays"; String TRACK_PERMISSIONS = "/tracks/%d/permissions"; + String PLAYLISTS = "/playlists"; + String PLAYLIST_DETAILS = "/playlists/%d"; + String PLAYLIST_TRACKS = "/playlists/%d/tracks"; + String USERS = "/users"; String USER_DETAILS = "/users/%d"; String USER_FOLLOWINGS = "/users/%d/followings"; From b53338643fd4d02543a9c980540730bfafa17a1d Mon Sep 17 00:00:00 2001 From: Jan Berkel Date: Tue, 5 Feb 2013 15:19:47 +0100 Subject: [PATCH 108/156] Support gzip encoding --- build.gradle | 2 +- .../java/com/soundcloud/api/ApiWrapper.java | 44 ++++++++++- .../java/com/soundcloud/api/CloudAPI.java | 1 + .../soundcloud/api/DecompressingEntity.java | 74 +++++++++++++++++++ .../api/GzipDecompressingEntity.java | 45 +++++++++++ src/main/java/com/soundcloud/api/Request.java | 1 - .../api/CloudAPIIntegrationTest.java | 26 +++++++ 7 files changed, 188 insertions(+), 5 deletions(-) create mode 100644 src/main/java/com/soundcloud/api/DecompressingEntity.java create mode 100644 src/main/java/com/soundcloud/api/GzipDecompressingEntity.java diff --git a/build.gradle b/build.gradle index 3e8987a..dc10f40 100644 --- a/build.gradle +++ b/build.gradle @@ -4,7 +4,7 @@ apply plugin: 'eclipse' apply plugin: 'maven' sourceCompatibility = 1.6 -version = '1.2.1' +version = '1.2.2-SNAPSHOT' group = 'com.soundcloud' repositories { mavenCentral() } diff --git a/src/main/java/com/soundcloud/api/ApiWrapper.java b/src/main/java/com/soundcloud/api/ApiWrapper.java index 93ba560..f4e53ee 100644 --- a/src/main/java/com/soundcloud/api/ApiWrapper.java +++ b/src/main/java/com/soundcloud/api/ApiWrapper.java @@ -2,8 +2,12 @@ import org.apache.http.ConnectionReuseStrategy; import org.apache.http.Header; +import org.apache.http.HeaderElement; +import org.apache.http.HttpEntity; +import org.apache.http.HttpException; import org.apache.http.HttpHost; import org.apache.http.HttpResponse; +import org.apache.http.HttpResponseInterceptor; import org.apache.http.HttpStatus; import org.apache.http.NameValuePair; import org.apache.http.auth.AUTH; @@ -97,6 +101,7 @@ public class ApiWrapper implements CloudAPI, Serializable { transient private TokenListener listener; private String mDefaultContentType; + private String mDefaultAcceptEncoding; public static final int BUFFER_SIZE = 8192; /** Connection timeout */ @@ -266,7 +271,7 @@ protected Token requestToken(Request request) throws IOException { HttpResponse response = safeExecute(env.sslResourceHost, request.buildRequest(HttpPost.class)); final int status = response.getStatusLine().getStatusCode(); - String error = null; + String error; try { if (status == HttpStatus.SC_OK) { final Token token = new Token(Http.getJSON(response)); @@ -402,6 +407,25 @@ public long getKeepAliveDuration(HttpResponse httpResponse, HttpContext httpCont OAuth2Scheme.EmptyCredentials.INSTANCE); getAuthSchemes().register(CloudAPI.OAUTH_SCHEME, new OAuth2Scheme.Factory(ApiWrapper.this)); + + addResponseInterceptor(new HttpResponseInterceptor() { + @Override + public void process(HttpResponse response, HttpContext context) + throws HttpException, IOException { + if (response == null || response.getEntity() == null) return; + + HttpEntity entity = response.getEntity(); + Header header = entity.getContentEncoding(); + if (header != null) { + for (HeaderElement codec : header.getElements()) { + if (codec.getName().equalsIgnoreCase("gzip")) { + response.setEntity(new GzipDecompressingEntity(entity)); + break; + } + } + } + } + }); } @Override protected HttpContext createHttpContext() { @@ -625,6 +649,15 @@ public void setDefaultContentType(String contentType) { mDefaultContentType = contentType; } + public String getDefaultAcceptEncoding() { + return mDefaultAcceptEncoding; + } + + public void setDefaultAcceptEncoding(String encoding) { + mDefaultAcceptEncoding = encoding; + } + + /* package */ static Request addScope(Request request, String[] scopes) { if (scopes != null && scopes.length > 0) { StringBuilder scope = new StringBuilder(); @@ -679,10 +712,15 @@ protected HttpUriRequest addAcceptHeader(HttpUriRequest request) { /** Adds all required headers to the request */ protected HttpUriRequest addHeaders(HttpUriRequest req) { - return addAcceptHeader( - addAuthHeader(req)); + return addAcceptHeader(addAuthHeader(addEncodingHeader(req))); } + protected HttpUriRequest addEncodingHeader(HttpUriRequest req) { + if (getDefaultAcceptEncoding() != null) { + req.addHeader("Accept-Encoding", getDefaultAcceptEncoding()); + } + return req; + } /** This method mainly exists to make the wrapper more testable. oh, apache's insanity. */ protected RequestDirector getRequestDirector(HttpRequestExecutor requestExec, diff --git a/src/main/java/com/soundcloud/api/CloudAPI.java b/src/main/java/com/soundcloud/api/CloudAPI.java index 4778791..dcf24be 100644 --- a/src/main/java/com/soundcloud/api/CloudAPI.java +++ b/src/main/java/com/soundcloud/api/CloudAPI.java @@ -227,6 +227,7 @@ public interface CloudAPI { * @param contentType the request mime type. */ void setDefaultContentType(String contentType); + void setDefaultAcceptEncoding(String encoding); /** * Interested in changes to the current token. diff --git a/src/main/java/com/soundcloud/api/DecompressingEntity.java b/src/main/java/com/soundcloud/api/DecompressingEntity.java new file mode 100644 index 0000000..a6e0e01 --- /dev/null +++ b/src/main/java/com/soundcloud/api/DecompressingEntity.java @@ -0,0 +1,74 @@ +package com.soundcloud.api; + +import org.apache.http.HttpEntity; +import org.apache.http.entity.HttpEntityWrapper; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; + +abstract class DecompressingEntity extends HttpEntityWrapper { + /** + * Default buffer size. + */ + private static final int BUFFER_SIZE = 1024 * 2; + + /** + * {@link #getContent()} method must return the same {@link InputStream} + * instance when DecompressingEntity is wrapping a streaming entity. + */ + private InputStream content; + + /** + * Creates a new {@link DecompressingEntity}. + * + * @param wrapped the non-null {@link HttpEntity} to be wrapped + */ + public DecompressingEntity(final HttpEntity wrapped) { + super(wrapped); + } + + abstract InputStream decorate(final InputStream wrapped) throws IOException; + + private InputStream getDecompressingStream() throws IOException { + final InputStream in = wrappedEntity.getContent(); + try { + return decorate(in); + } catch (final IOException ex) { + in.close(); + throw ex; + } + } + + /** + * {@inheritDoc} + */ + @Override + public InputStream getContent() throws IOException { + if (wrappedEntity.isStreaming()) { + if (content == null) { + content = getDecompressingStream(); + } + return content; + } else { + return getDecompressingStream(); + } + } + + /** + * {@inheritDoc} + */ + @Override + public void writeTo(final OutputStream outstream) throws IOException { + final InputStream instream = getContent(); + try { + final byte[] buffer = new byte[BUFFER_SIZE]; + int l; + while ((l = instream.read(buffer)) != -1) { + outstream.write(buffer, 0, l); + } + } finally { + instream.close(); + } + } +} diff --git a/src/main/java/com/soundcloud/api/GzipDecompressingEntity.java b/src/main/java/com/soundcloud/api/GzipDecompressingEntity.java new file mode 100644 index 0000000..61c1b84 --- /dev/null +++ b/src/main/java/com/soundcloud/api/GzipDecompressingEntity.java @@ -0,0 +1,45 @@ +package com.soundcloud.api; + +import org.apache.http.Header; +import org.apache.http.HttpEntity; + +import java.io.IOException; +import java.io.InputStream; +import java.util.zip.GZIPInputStream; + +public class GzipDecompressingEntity extends DecompressingEntity { + + /** + * Creates a new {@link GzipDecompressingEntity} which will wrap the specified + * {@link HttpEntity}. + * + * @param entity + * the non-null {@link HttpEntity} to be wrapped + */ + public GzipDecompressingEntity(final HttpEntity entity) { + super(entity); + } + + @Override + InputStream decorate(final InputStream wrapped) throws IOException { + return new GZIPInputStream(wrapped); + } + + /** + * {@inheritDoc} + */ + @Override + public Header getContentEncoding() { + /* This HttpEntityWrapper has dealt with the Content-Encoding. */ + return null; + } + + /** + * {@inheritDoc} + */ + @Override + public long getContentLength() { + /* length of ungzipped content is not known */ + return -1; + } +} \ No newline at end of file diff --git a/src/main/java/com/soundcloud/api/Request.java b/src/main/java/com/soundcloud/api/Request.java index 697beff..8afaedb 100644 --- a/src/main/java/com/soundcloud/api/Request.java +++ b/src/main/java/com/soundcloud/api/Request.java @@ -425,7 +425,6 @@ public T buildRequest(Class method) { } catch (IllegalAccessException e) { throw new RuntimeException(e); } catch (UnsupportedEncodingException e) { - // XXX really rethrow? throw new RuntimeException(e); } } diff --git a/src/test/java/com/soundcloud/api/CloudAPIIntegrationTest.java b/src/test/java/com/soundcloud/api/CloudAPIIntegrationTest.java index b120869..b81f2c2 100644 --- a/src/test/java/com/soundcloud/api/CloudAPIIntegrationTest.java +++ b/src/test/java/com/soundcloud/api/CloudAPIIntegrationTest.java @@ -9,7 +9,9 @@ import org.apache.http.Header; import org.apache.http.HttpResponse; import org.apache.http.client.methods.HttpGet; +import org.json.JSONArray; import org.json.JSONObject; +import org.json.JSONTokener; import org.junit.Before; import org.junit.Ignore; import org.junit.Test; @@ -19,6 +21,7 @@ import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; +import java.io.InputStreamReader; import java.net.UnknownHostException; import java.nio.ByteBuffer; import java.util.concurrent.BrokenBarrierException; @@ -54,6 +57,7 @@ public void setUp() throws Exception { CLIENT_SECRET, null, null); + } private Token login(String... scopes) throws IOException { @@ -245,6 +249,28 @@ public void readMyDetails() throws Exception { // writeResponse(resp, "me.json"); } + @Test + public void readGzipCompressedData() throws Exception { + api.setDefaultAcceptEncoding("gzip"); + + login(); + + HttpResponse resp = api.get(Request.to(Endpoints.TRACKS)); + assertThat(resp.getStatusLine().getStatusCode(), is(200)); + + assertThat( + resp.getFirstHeader("Content-Type").getValue(), + containsString("application/json")); + + assertThat( + resp.getFirstHeader("Content-Encoding").getValue(), + equalTo("gzip")); + + JSONArray array = new JSONArray(new JSONTokener(new InputStreamReader(resp.getEntity().getContent()))); + + assertTrue("array is empty", array.length() > 0); + } + @Test public void shouldLoginWithNonExpiringScope() throws Exception { Token token = login(Token.SCOPE_NON_EXPIRING); From bfbfd6ba8b7e4aabbc70d94aad8879611ae37440 Mon Sep 17 00:00:00 2001 From: Jan Berkel Date: Tue, 5 Feb 2013 16:01:57 +0100 Subject: [PATCH 109/156] Changelog --- CHANGES.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/CHANGES.md b/CHANGES.md index 20224b9..6de2919 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,3 +1,8 @@ +## 1.2.2 TBD + + * Support gzip encoding + * Fixed non-english request formatting issue + ## 1.2.1 2013-01-10 * Added ApiResponseException to wrap HTTP error codes when logging in From f7e45e6766a19e2c3bea595f28355191e76d4c95 Mon Sep 17 00:00:00 2001 From: jonschmidt Date: Tue, 12 Feb 2013 16:14:08 +0100 Subject: [PATCH 110/156] maintain entity through put process --- src/main/java/com/soundcloud/api/Request.java | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/main/java/com/soundcloud/api/Request.java b/src/main/java/com/soundcloud/api/Request.java index 8afaedb..0c348be 100644 --- a/src/main/java/com/soundcloud/api/Request.java +++ b/src/main/java/com/soundcloud/api/Request.java @@ -115,6 +115,7 @@ public Request(Request request) { listener = request.listener; mParams = new ArrayList(request.mParams); mIfNoneMatch = request.mIfNoneMatch; + mEntity = request.mEntity; if (request.mFiles != null) mFiles = new HashMap(request.mFiles); } @@ -396,12 +397,13 @@ public T buildRequest(Class method) { enclosingRequest.setEntity(listener == null ? multiPart : new CountingMultipartEntity(multiPart, listener)); // form-urlencoded? - } else if (!mParams.isEmpty()) { - request.setHeader("Content-Type", "application/x-www-form-urlencoded"); - enclosingRequest.setEntity(new StringEntity(queryString())); } else if (mEntity != null) { request.setHeader(mEntity.getContentType()); enclosingRequest.setEntity(mEntity); + + } else if (!mParams.isEmpty()) { + request.setHeader("Content-Type", "application/x-www-form-urlencoded"); + enclosingRequest.setEntity(new StringEntity(queryString())); } request.setURI(URI.create(mResource)); From a8dfbc5d7ab951457d2df21d0b96b770c6d1a427 Mon Sep 17 00:00:00 2001 From: jonschmidt Date: Tue, 12 Feb 2013 16:34:41 +0100 Subject: [PATCH 111/156] keep params in entity request --- src/main/java/com/soundcloud/api/Request.java | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/main/java/com/soundcloud/api/Request.java b/src/main/java/com/soundcloud/api/Request.java index 0c348be..10650e3 100644 --- a/src/main/java/com/soundcloud/api/Request.java +++ b/src/main/java/com/soundcloud/api/Request.java @@ -396,17 +396,22 @@ public T buildRequest(Class method) { enclosingRequest.setEntity(listener == null ? multiPart : new CountingMultipartEntity(multiPart, listener)); + + request.setURI(URI.create(mResource)); + // form-urlencoded? } else if (mEntity != null) { request.setHeader(mEntity.getContentType()); enclosingRequest.setEntity(mEntity); + request.setURI(URI.create(toUrl())); // include the params } else if (!mParams.isEmpty()) { request.setHeader("Content-Type", "application/x-www-form-urlencoded"); enclosingRequest.setEntity(new StringEntity(queryString())); + request.setURI(URI.create(mResource)); } - request.setURI(URI.create(mResource)); + } else { // just plain GET/HEAD/DELETE/... if (mRange != null) { request.addHeader("Range", formatRange(mRange)); From d333a936a58391fd373c48d156d2fb1bc6d7e253 Mon Sep 17 00:00:00 2001 From: Jan Berkel Date: Tue, 12 Feb 2013 16:54:40 +0100 Subject: [PATCH 112/156] Added a test for entity fix --- src/test/java/com/soundcloud/api/RequestTest.java | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/src/test/java/com/soundcloud/api/RequestTest.java b/src/test/java/com/soundcloud/api/RequestTest.java index 06e1275..0121b8b 100644 --- a/src/test/java/com/soundcloud/api/RequestTest.java +++ b/src/test/java/com/soundcloud/api/RequestTest.java @@ -15,6 +15,7 @@ import org.apache.http.client.methods.HttpPost; import org.apache.http.entity.StringEntity; import org.apache.http.entity.mime.MultipartEntity; +import org.apache.http.util.EntityUtils; import org.hamcrest.CoreMatchers; import org.junit.Ignore; import org.junit.Test; @@ -259,6 +260,20 @@ public void shouldIncludeContentInRequest() throws Exception { assertThat("content", equalTo(body)); } + @Test + public void shouldBuildARequestWithContentAndPreserveQueryParameters() throws Exception { + HttpPost post = Request + .to("/foo") + .withContent("{}", "application/json") + .with("1", "2").buildRequest(HttpPost.class); + + assertThat(post.getURI().toString(), equalTo("/foo?1=2")); + assertTrue(post.getEntity() instanceof StringEntity); + assertThat(post.getEntity().getContentLength(), equalTo(2l)); + assertThat(EntityUtils.toString(post.getEntity()), equalTo("{}")); + assertThat(post.getFirstHeader("Content-Type").getValue(), equalTo("application/json")); + } + @Test public void whenAProgressListenerIsSpecifiedShouldHaveCountingMultipart() throws Exception { HttpPost request = Request.to("/foo") From f7f88e46f6db3326e51798030f347f8370be44db Mon Sep 17 00:00:00 2001 From: Jan Berkel Date: Tue, 12 Feb 2013 17:00:43 +0100 Subject: [PATCH 113/156] changelog --- CHANGES.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGES.md b/CHANGES.md index 6de2919..59f394f 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -2,6 +2,7 @@ * Support gzip encoding * Fixed non-english request formatting issue + * Preserve query parameters for 'withContent` ## 1.2.1 2013-01-10 From bd2b45b3ebc32653e0a1b405e0cf521f5c8f077d Mon Sep 17 00:00:00 2001 From: jonschmidt Date: Tue, 12 Feb 2013 17:50:03 +0100 Subject: [PATCH 114/156] fixed setUri logic in buildRequest --- src/main/java/com/soundcloud/api/Request.java | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/main/java/com/soundcloud/api/Request.java b/src/main/java/com/soundcloud/api/Request.java index 10650e3..c4d5f17 100644 --- a/src/main/java/com/soundcloud/api/Request.java +++ b/src/main/java/com/soundcloud/api/Request.java @@ -405,13 +405,14 @@ public T buildRequest(Class method) { enclosingRequest.setEntity(mEntity); request.setURI(URI.create(toUrl())); // include the params - } else if (!mParams.isEmpty()) { - request.setHeader("Content-Type", "application/x-www-form-urlencoded"); - enclosingRequest.setEntity(new StringEntity(queryString())); + } else { + if (!mParams.isEmpty()) { + request.setHeader("Content-Type", "application/x-www-form-urlencoded"); + enclosingRequest.setEntity(new StringEntity(queryString())); + } request.setURI(URI.create(mResource)); } - } else { // just plain GET/HEAD/DELETE/... if (mRange != null) { request.addHeader("Range", formatRange(mRange)); From eae26783a86cc08d12e25eece90d3e1fe2547979 Mon Sep 17 00:00:00 2001 From: jonschmidt Date: Tue, 12 Feb 2013 17:59:48 +0100 Subject: [PATCH 115/156] test for preserving URI --- src/test/java/com/soundcloud/api/RequestTest.java | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/src/test/java/com/soundcloud/api/RequestTest.java b/src/test/java/com/soundcloud/api/RequestTest.java index 0121b8b..ff4acbd 100644 --- a/src/test/java/com/soundcloud/api/RequestTest.java +++ b/src/test/java/com/soundcloud/api/RequestTest.java @@ -3,6 +3,7 @@ import static org.hamcrest.CoreMatchers.*; import static org.junit.Assert.*; +import static org.mockito.Matchers.notNull; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; @@ -215,6 +216,15 @@ public void shouldUploadByteDataWithFilename() throws Exception { assertThat(encoded, containsString("filename=\"music.mp3\"")); } + @Test + public void shouldPreservePostUri() throws Exception { + HttpPost request = Request.to("/foo") + .buildRequest(HttpPost.class); + + assertThat(request.getURI(), notNullValue()); + assertThat(request.getURI().toString(), equalTo("/foo")); + } + @Test public void shouldCreateMultipartRequestWhenFilesAreAddedWithByteArray() throws Exception { HttpPost request = Request.to("/foo") @@ -274,6 +284,8 @@ public void shouldBuildARequestWithContentAndPreserveQueryParameters() throws Ex assertThat(post.getFirstHeader("Content-Type").getValue(), equalTo("application/json")); } + + @Test public void whenAProgressListenerIsSpecifiedShouldHaveCountingMultipart() throws Exception { HttpPost request = Request.to("/foo") From b2bead043b48f8c278cfe0f76a1e060a7e4247c4 Mon Sep 17 00:00:00 2001 From: Jan Berkel Date: Wed, 13 Feb 2013 15:59:33 +0100 Subject: [PATCH 116/156] Add some tests/sample code for playlist creation --- .../api/CloudAPIIntegrationTest.java | 85 +++++++++++++++++++ 1 file changed, 85 insertions(+) diff --git a/src/test/java/com/soundcloud/api/CloudAPIIntegrationTest.java b/src/test/java/com/soundcloud/api/CloudAPIIntegrationTest.java index b81f2c2..cf6a12b 100644 --- a/src/test/java/com/soundcloud/api/CloudAPIIntegrationTest.java +++ b/src/test/java/com/soundcloud/api/CloudAPIIntegrationTest.java @@ -9,7 +9,9 @@ import org.apache.http.Header; import org.apache.http.HttpResponse; import org.apache.http.client.methods.HttpGet; +import org.apache.http.util.EntityUtils; import org.json.JSONArray; +import org.json.JSONException; import org.json.JSONObject; import org.json.JSONTokener; import org.junit.Before; @@ -35,6 +37,8 @@ public class CloudAPIIntegrationTest implements Params.Track, Endpoints { public static final String TRACK_PERMALINK = "http://soundcloud.com/jberkel/nobody-home"; public static final long USER_ID = 18173653L; + public static final long CHE_FLUTE_TRACK_ID = 274334; + public static final long FLICKERMOOD_TRACK_ID = 293; public static final long TRACK_LENGTH = 224861L; CloudAPI api; @@ -85,6 +89,68 @@ public void shouldUploadASimpleAudioFile() throws Exception { assertNotNull(location); } + @Test + public void shouldCreateAPlaylistAndAddTracksToIt() throws Exception { + login(); + + HttpResponse resp = api.post(Request.to(PLAYLISTS) + .with("playlist[title]", "test playlist")); + + int status = resp.getStatusLine().getStatusCode(); + assertThat(status, is(201)); + + Header location = resp.getFirstHeader("Location"); + assertNotNull(location); + + String playlistUrl = location.getValue(); + assertNotNull(playlistUrl); + + String title = "a new title:" + System.currentTimeMillis(); + resp = api.put(Request.to(playlistUrl) + .with("playlist[title]", title) + .with("playlist[tracks][][id]", CHE_FLUTE_TRACK_ID) + .with("playlist[tracks][][id]", FLICKERMOOD_TRACK_ID)); + + status = resp.getStatusLine().getStatusCode(); + assertThat(status, is(200)); + + JSONObject obj = new JSONObject(EntityUtils.toString(resp.getEntity())); + assertThat(obj.getString("kind"), equalTo("playlist")); + assertThat(obj.getString("title"), equalTo(title)); + assertThat(obj.getInt("track_count"), equalTo(2)); + } + + @Test + public void shouldCreateAPlaylistAndAddTracksToItWithJSON() throws Exception { + login(); + + HttpResponse resp = api.post(Request.to(PLAYLISTS) + .with("playlist[title]", "test playlist")); + + int status = resp.getStatusLine().getStatusCode(); + assertThat(status, is(201)); + + Header location = resp.getFirstHeader("Location"); + assertNotNull(location); + + String playlistUrl = location.getValue(); + assertNotNull(playlistUrl); + + String title = "a new title:" + System.currentTimeMillis(); + JSONObject json = createJSONPlaylist(title, CHE_FLUTE_TRACK_ID, FLICKERMOOD_TRACK_ID); + + resp = api.put(Request.to(playlistUrl) + .withContent(json.toString(), "application/json")); + + status = resp.getStatusLine().getStatusCode(); + assertThat(status, is(200)); + + JSONObject obj = new JSONObject(EntityUtils.toString(resp.getEntity())); + assertThat(obj.getString("kind"), equalTo("playlist")); + assertThat(obj.getString("title"), equalTo(title)); + assertThat(obj.getInt("track_count"), equalTo(2)); + } + @Test public void shouldUploadASimpleAudioFileBytes() throws Exception { login(); @@ -396,4 +462,23 @@ private String getApiUrlFromPermalink(String permalink) throws IOException { long trackId = api.resolve(permalink); return "https://api.soundcloud.com/tracks/" + trackId; } + + + private JSONObject createJSONPlaylist(String title, long... trackIds) throws JSONException { + JSONObject playlist = new JSONObject(); + playlist.put("title", title); + + JSONObject json = new JSONObject(); + json.put("playlist", playlist); + + JSONArray tracks = new JSONArray(); + playlist.put("tracks", tracks); + + for (long id : trackIds) { + JSONObject track = new JSONObject(); + track.put("id", id); + tracks.put(track); + } + return json; + } } From be64bc0f47814acc7759fcfed66f4be4cb6681b3 Mon Sep 17 00:00:00 2001 From: jonschmidt Date: Tue, 2 Apr 2013 15:46:26 +0200 Subject: [PATCH 117/156] new secure streaming expectations --- src/test/java/com/soundcloud/api/CloudAPIIntegrationTest.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/test/java/com/soundcloud/api/CloudAPIIntegrationTest.java b/src/test/java/com/soundcloud/api/CloudAPIIntegrationTest.java index cf6a12b..6396403 100644 --- a/src/test/java/com/soundcloud/api/CloudAPIIntegrationTest.java +++ b/src/test/java/com/soundcloud/api/CloudAPIIntegrationTest.java @@ -240,7 +240,7 @@ public void shouldResolveStreamUrls() throws Exception { Stream resolved = api.resolveStreamUrl(streamUrl, false); assertThat(resolved.url, equalTo(streamUrl)); - assertThat(resolved.streamUrl, containsString("http://ec-media.soundcloud.com/")); + assertThat(resolved.streamUrl, containsString("https://ec-media.soundcloud.com/")); assertTrue("expire should be in the future", resolved.expires > System.currentTimeMillis()); assertThat(resolved.eTag, equalTo("\"980f61d6d6ee26ffe0c78aef618d786f\"")); From 275f9fbaf117565713e377de227bd4193e9deb16 Mon Sep 17 00:00:00 2001 From: jonschmidt Date: Tue, 2 Apr 2013 16:38:57 +0200 Subject: [PATCH 118/156] add consumer key if no valid token present --- src/main/java/com/soundcloud/api/ApiWrapper.java | 7 ++++++- src/test/java/com/soundcloud/api/ApiWrapperTest.java | 10 +++++----- 2 files changed, 11 insertions(+), 6 deletions(-) diff --git a/src/main/java/com/soundcloud/api/ApiWrapper.java b/src/main/java/com/soundcloud/api/ApiWrapper.java index f4e53ee..6c3d27e 100644 --- a/src/main/java/com/soundcloud/api/ApiWrapper.java +++ b/src/main/java/com/soundcloud/api/ApiWrapper.java @@ -607,7 +607,12 @@ protected HttpResponse execute(Request req, Class req } } logRequest(reqType, req); - return execute(req.buildRequest(reqType)); + if (mToken == EMPTY_TOKEN){ + return execute(new Request(req).add("consumer_key", mClientId).buildRequest(reqType)); + } else { + return execute(req.buildRequest(reqType)); + } + } diff --git a/src/test/java/com/soundcloud/api/ApiWrapperTest.java b/src/test/java/com/soundcloud/api/ApiWrapperTest.java index e4d3dae..424e2b7 100644 --- a/src/test/java/com/soundcloud/api/ApiWrapperTest.java +++ b/src/test/java/com/soundcloud/api/ApiWrapperTest.java @@ -265,7 +265,7 @@ public void resolveShouldReturnNegativeOneWhenInvalid() throws Exception { @Test public void shouldGetContent() throws Exception { - layer.addHttpResponseRule("/some/resource?a=1", "response"); + layer.addHttpResponseRule("/some/resource?a=1&consumer_key=invalid", "response"); assertThat(Http.getString(api.get(Request.to("/some/resource").with("a", "1"))), equalTo("response")); } @@ -289,7 +289,7 @@ public void shouldPutContent() throws Exception { @Test public void shouldDeleteContent() throws Exception { HttpResponse resp = mock(HttpResponse.class); - layer.addHttpResponseRule("DELETE", "/foo/something", resp); + layer.addHttpResponseRule("DELETE", "/foo/something?consumer_key=invalid", resp); assertThat(api.delete(new Request("/foo/something")), equalTo(resp)); } @@ -518,9 +518,9 @@ public HttpClient getHttpClient() { @Test public void testAddDefaultParameters() throws Exception { - layer.addHttpResponseRule("/foo", "Hi"); - layer.addHttpResponseRule("/foo?t=1", "Hi t1"); - layer.addHttpResponseRule("/foo?t=2", "Hi t2"); + layer.addHttpResponseRule("/foo?consumer_key=invalid", "Hi"); + layer.addHttpResponseRule("/foo?t=1&consumer_key=invalid", "Hi t1"); + layer.addHttpResponseRule("/foo?t=2&consumer_key=invalid", "Hi t2"); final Request foo = Request.to("/foo"); for (int i = 0; i < 1000; i++) { From b5c70449be395caa03607627fc211b4d850295cc Mon Sep 17 00:00:00 2001 From: jonschmidt Date: Fri, 12 Apr 2013 18:14:46 +0200 Subject: [PATCH 119/156] * added google plus grant type test still pending? --- src/main/java/com/soundcloud/api/CloudAPI.java | 3 ++- .../java/com/soundcloud/api/CloudAPIIntegrationTest.java | 6 +++--- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/src/main/java/com/soundcloud/api/CloudAPI.java b/src/main/java/com/soundcloud/api/CloudAPI.java index dcf24be..ef2727c 100644 --- a/src/main/java/com/soundcloud/api/CloudAPI.java +++ b/src/main/java/com/soundcloud/api/CloudAPI.java @@ -25,7 +25,8 @@ public interface CloudAPI { String OAUTH1_TOKEN = "oauth1_token"; // oauth2 extension grant types - String FACEBOOK_GRANT_TYPE = "urn:soundcloud:oauth2:grant-type:facebook&access_token="; + String FACEBOOK_GRANT_TYPE = "urn:soundcloud:oauth2:grant-type:facebook&access_token="; + String GOOGLE_GRANT_TYPE = "urn:soundcloud:oauth2:grant-type:google_plus&access_token="; // other constants String REALM = "SoundCloud"; diff --git a/src/test/java/com/soundcloud/api/CloudAPIIntegrationTest.java b/src/test/java/com/soundcloud/api/CloudAPIIntegrationTest.java index 6396403..6f408b7 100644 --- a/src/test/java/com/soundcloud/api/CloudAPIIntegrationTest.java +++ b/src/test/java/com/soundcloud/api/CloudAPIIntegrationTest.java @@ -178,9 +178,9 @@ public void shouldNotGetASignupTokenWhenInofficialApp() throws Exception { @Test(expected = CloudAPI.InvalidTokenException.class) public void shouldGetATokenUsingExtensionGrantTypes() throws Exception { - // TODO - String fbToken = "fbToken"; - api.extensionGrantType(CloudAPI.FACEBOOK_GRANT_TYPE +fbToken); + // TODO ? + api.extensionGrantType(CloudAPI.FACEBOOK_GRANT_TYPE + "fbToken"); + api.extensionGrantType(CloudAPI.GOOGLE_GRANT_TYPE + "googleToken"); } @Test From 7a7367c145ec5585af07ce02eacefc01b8fe6c43 Mon Sep 17 00:00:00 2001 From: jonschmidt Date: Wed, 17 Apr 2013 13:35:27 +0200 Subject: [PATCH 120/156] small refactors --- .../java/com/soundcloud/api/ApiWrapper.java | 8 +++----- .../com/soundcloud/api/ApiWrapperTest.java | 20 ++++++++++--------- 2 files changed, 14 insertions(+), 14 deletions(-) diff --git a/src/main/java/com/soundcloud/api/ApiWrapper.java b/src/main/java/com/soundcloud/api/ApiWrapper.java index 6c3d27e..15c2929 100644 --- a/src/main/java/com/soundcloud/api/ApiWrapper.java +++ b/src/main/java/com/soundcloud/api/ApiWrapper.java @@ -607,12 +607,10 @@ protected HttpResponse execute(Request req, Class req } } logRequest(reqType, req); - if (mToken == EMPTY_TOKEN){ - return execute(new Request(req).add("consumer_key", mClientId).buildRequest(reqType)); - } else { - return execute(req.buildRequest(reqType)); + if (mToken == EMPTY_TOKEN) { + req = new Request(req).add("client_id", mClientId); } - + return execute(req.buildRequest(reqType)); } diff --git a/src/test/java/com/soundcloud/api/ApiWrapperTest.java b/src/test/java/com/soundcloud/api/ApiWrapperTest.java index 424e2b7..0ead6c9 100644 --- a/src/test/java/com/soundcloud/api/ApiWrapperTest.java +++ b/src/test/java/com/soundcloud/api/ApiWrapperTest.java @@ -46,10 +46,12 @@ public class ApiWrapperTest { private ApiWrapper api; + private static String TEST_CLIENT_ID = "testClientId"; + private static String TEST_CLIENT_SECRET = "testClientSecret"; final FakeHttpLayer layer = new FakeHttpLayer(); @Before public void setup() { - api = new ApiWrapper("invalid", "invalid", URI.create("redirect://me"), null) { + api = new ApiWrapper(TEST_CLIENT_ID, TEST_CLIENT_SECRET, URI.create("redirect://me"), null) { private static final long serialVersionUID = 12345; // silence warnings @Override protected RequestDirector getRequestDirector(HttpRequestExecutor requestExec, @@ -265,7 +267,7 @@ public void resolveShouldReturnNegativeOneWhenInvalid() throws Exception { @Test public void shouldGetContent() throws Exception { - layer.addHttpResponseRule("/some/resource?a=1&consumer_key=invalid", "response"); + layer.addHttpResponseRule("/some/resource?a=1&client_id=" + TEST_CLIENT_ID, "response"); assertThat(Http.getString(api.get(Request.to("/some/resource").with("a", "1"))), equalTo("response")); } @@ -289,7 +291,7 @@ public void shouldPutContent() throws Exception { @Test public void shouldDeleteContent() throws Exception { HttpResponse resp = mock(HttpResponse.class); - layer.addHttpResponseRule("DELETE", "/foo/something?consumer_key=invalid", resp); + layer.addHttpResponseRule("DELETE", "/foo/something?client_id=" + TEST_CLIENT_ID, resp); assertThat(api.delete(new Request("/foo/something")), equalTo(resp)); } @@ -345,7 +347,7 @@ public void shouldGenerateURIForLoginAuthCode() throws Exception { assertThat( api.authorizationCodeUrl().toString(), equalTo("https://soundcloud.com/connect"+ - "?redirect_uri=redirect%3A%2F%2Fme&client_id=invalid&response_type=code") + "?redirect_uri=redirect%3A%2F%2Fme&client_id=" + TEST_CLIENT_ID + "&response_type=code") ); } @@ -355,7 +357,7 @@ public void shouldGenerateURIForLoginAuthCodeWithDifferentEndPoint() throws Exce assertThat( api.authorizationCodeUrl(Endpoints.FACEBOOK_CONNECT).toString(), equalTo("https://soundcloud.com/connect/via/facebook"+ - "?redirect_uri=redirect%3A%2F%2Fme&client_id=invalid&response_type=code") + "?redirect_uri=redirect%3A%2F%2Fme&client_id=" + TEST_CLIENT_ID + "&response_type=code") ); } @@ -364,7 +366,7 @@ public void shouldIncludeScopeInAuthorizationUrl() throws Exception { assertThat( api.authorizationCodeUrl(Endpoints.FACEBOOK_CONNECT, Token.SCOPE_NON_EXPIRING).toString(), equalTo("https://soundcloud.com/connect/via/facebook"+ - "?redirect_uri=redirect%3A%2F%2Fme&client_id=invalid&response_type=code&scope=non-expiring") + "?redirect_uri=redirect%3A%2F%2Fme&client_id=" + TEST_CLIENT_ID + "&response_type=code&scope=non-expiring") ); } @@ -518,9 +520,9 @@ public HttpClient getHttpClient() { @Test public void testAddDefaultParameters() throws Exception { - layer.addHttpResponseRule("/foo?consumer_key=invalid", "Hi"); - layer.addHttpResponseRule("/foo?t=1&consumer_key=invalid", "Hi t1"); - layer.addHttpResponseRule("/foo?t=2&consumer_key=invalid", "Hi t2"); + layer.addHttpResponseRule("/foo?client_id=" + TEST_CLIENT_ID, "Hi"); + layer.addHttpResponseRule("/foo?t=1&client_id=" + TEST_CLIENT_ID, "Hi t1"); + layer.addHttpResponseRule("/foo?t=2&client_id=" + TEST_CLIENT_ID, "Hi t2"); final Request foo = Request.to("/foo"); for (int i = 0; i < 1000; i++) { From 759fa5c775e5f1fa3a77bdf5b379f85d659c85d9 Mon Sep 17 00:00:00 2001 From: jonschmidt Date: Wed, 17 Apr 2013 15:10:36 +0200 Subject: [PATCH 121/156] extract client id logic and test --- src/main/java/com/soundcloud/api/ApiWrapper.java | 8 ++++---- src/test/java/com/soundcloud/api/ApiWrapperTest.java | 7 +++++++ 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/src/main/java/com/soundcloud/api/ApiWrapper.java b/src/main/java/com/soundcloud/api/ApiWrapper.java index 15c2929..90cae3e 100644 --- a/src/main/java/com/soundcloud/api/ApiWrapper.java +++ b/src/main/java/com/soundcloud/api/ApiWrapper.java @@ -607,12 +607,12 @@ protected HttpResponse execute(Request req, Class req } } logRequest(reqType, req); - if (mToken == EMPTY_TOKEN) { - req = new Request(req).add("client_id", mClientId); - } - return execute(req.buildRequest(reqType)); + return execute(addClientIdIfNecessary(req).buildRequest(reqType)); } + protected Request addClientIdIfNecessary(Request req) { + return (mToken != EMPTY_TOKEN) ? req : new Request(req).add("client_id", mClientId); + } protected void logRequest( Class reqType, Request request) { if (debugRequests) System.err.println(reqType.getSimpleName()+" "+request); diff --git a/src/test/java/com/soundcloud/api/ApiWrapperTest.java b/src/test/java/com/soundcloud/api/ApiWrapperTest.java index 0ead6c9..462eeb4 100644 --- a/src/test/java/com/soundcloud/api/ApiWrapperTest.java +++ b/src/test/java/com/soundcloud/api/ApiWrapperTest.java @@ -518,6 +518,13 @@ public HttpClient getHttpClient() { } } + @Test + public void testOnlyAddClientIdWithoutToken() throws Exception { + assertThat(api.addClientIdIfNecessary(Request.to("/foo")).toUrl(), equalTo("/foo?client_id=" + TEST_CLIENT_ID)); + api.setToken(new Token("access", "refresh")); + assertThat(api.addClientIdIfNecessary(Request.to("/foo")).toUrl(), equalTo("/foo")); + } + @Test public void testAddDefaultParameters() throws Exception { layer.addHttpResponseRule("/foo?client_id=" + TEST_CLIENT_ID, "Hi"); From 06dfbcbdf4f4e84fa978dec0913d64b096eabf58 Mon Sep 17 00:00:00 2001 From: jonschmidt Date: Wed, 17 Apr 2013 15:16:15 +0200 Subject: [PATCH 122/156] rename grant, remove from test --- src/main/java/com/soundcloud/api/CloudAPI.java | 4 ++-- src/test/java/com/soundcloud/api/CloudAPIIntegrationTest.java | 1 - 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/src/main/java/com/soundcloud/api/CloudAPI.java b/src/main/java/com/soundcloud/api/CloudAPI.java index ef2727c..f17d99e 100644 --- a/src/main/java/com/soundcloud/api/CloudAPI.java +++ b/src/main/java/com/soundcloud/api/CloudAPI.java @@ -25,8 +25,8 @@ public interface CloudAPI { String OAUTH1_TOKEN = "oauth1_token"; // oauth2 extension grant types - String FACEBOOK_GRANT_TYPE = "urn:soundcloud:oauth2:grant-type:facebook&access_token="; - String GOOGLE_GRANT_TYPE = "urn:soundcloud:oauth2:grant-type:google_plus&access_token="; + String FACEBOOK_GRANT_TYPE = "urn:soundcloud:oauth2:grant-type:facebook&access_token="; + String GOOGLE_PLUS_GRANT_TYPE = "urn:soundcloud:oauth2:grant-type:google_plus&access_token="; // other constants String REALM = "SoundCloud"; diff --git a/src/test/java/com/soundcloud/api/CloudAPIIntegrationTest.java b/src/test/java/com/soundcloud/api/CloudAPIIntegrationTest.java index 6f408b7..52a2bbd 100644 --- a/src/test/java/com/soundcloud/api/CloudAPIIntegrationTest.java +++ b/src/test/java/com/soundcloud/api/CloudAPIIntegrationTest.java @@ -180,7 +180,6 @@ public void shouldNotGetASignupTokenWhenInofficialApp() throws Exception { public void shouldGetATokenUsingExtensionGrantTypes() throws Exception { // TODO ? api.extensionGrantType(CloudAPI.FACEBOOK_GRANT_TYPE + "fbToken"); - api.extensionGrantType(CloudAPI.GOOGLE_GRANT_TYPE + "googleToken"); } @Test From 538922ac3a9e3fdb8f8506ab9e26f9ad9bf82474 Mon Sep 17 00:00:00 2001 From: Jan Berkel Date: Thu, 18 Apr 2013 16:41:03 +0200 Subject: [PATCH 123/156] Default to UTF-8 for string payloads --- src/main/java/com/soundcloud/api/Request.java | 14 ++++++++------ src/main/java/com/soundcloud/api/Stream.java | 4 ++-- .../soundcloud/api/CloudAPIIntegrationTest.java | 2 +- src/test/java/com/soundcloud/api/RequestTest.java | 13 +++++++++++++ 4 files changed, 24 insertions(+), 9 deletions(-) diff --git a/src/main/java/com/soundcloud/api/Request.java b/src/main/java/com/soundcloud/api/Request.java index c4d5f17..e0ee8c9 100644 --- a/src/main/java/com/soundcloud/api/Request.java +++ b/src/main/java/com/soundcloud/api/Request.java @@ -47,6 +47,8 @@ * */ public class Request implements Iterable { + public static final String UTF_8 = "UTF-8"; + private List mParams = new ArrayList(); // XXX should probably be lazy private Map mFiles; @@ -82,10 +84,10 @@ public Request(String resource) { try { if (kv.length == 2) { mParams.add(new BasicNameValuePair( - URLDecoder.decode(kv[0], "UTF-8"), - URLDecoder.decode(kv[1], "UTF-8"))); + URLDecoder.decode(kv[0], UTF_8), + URLDecoder.decode(kv[1], UTF_8))); } else if (kv.length == 1) { - mParams.add(new BasicNameValuePair(URLDecoder.decode(kv[0], "UTF-8"), null)); + mParams.add(new BasicNameValuePair(URLDecoder.decode(kv[0], UTF_8), null)); } } catch (UnsupportedEncodingException ignored) { } @@ -215,7 +217,7 @@ public int size() { * list of parameters in an HTTP PUT or HTTP POST. */ public String queryString() { - return format(mParams, "UTF-8"); + return format(mParams, UTF_8); } /** @@ -317,7 +319,7 @@ public Request withEntity(HttpEntity entity) { */ public Request withContent(String content, String contentType) { try { - StringEntity stringEntity = new StringEntity(content); + StringEntity stringEntity = new StringEntity(content, UTF_8); if (contentType != null) { stringEntity.setContentType(contentType); } @@ -377,7 +379,7 @@ public T buildRequest(Class method) { HttpEntityEnclosingRequestBase enclosingRequest = (HttpEntityEnclosingRequestBase) request; - final Charset charSet = java.nio.charset.Charset.forName("UTF-8"); + final Charset charSet = java.nio.charset.Charset.forName(UTF_8); if (isMultipart()) { MultipartEntity multiPart = new MultipartEntity( HttpMultipartMode.BROWSER_COMPATIBLE, // XXX change this to STRICT once rack on server is upgraded diff --git a/src/main/java/com/soundcloud/api/Stream.java b/src/main/java/com/soundcloud/api/Stream.java index cab576f..a7a08fd 100644 --- a/src/main/java/com/soundcloud/api/Stream.java +++ b/src/main/java/com/soundcloud/api/Stream.java @@ -107,9 +107,9 @@ private static long getExpires(String resource) { String[] kv = s.split("=", 2); if (kv != null && kv.length == 2) { try { - String name = URLDecoder.decode(kv[0], "UTF-8"); + String name = URLDecoder.decode(kv[0], Request.UTF_8); if (EXPIRES.equalsIgnoreCase(name)) { - String value = URLDecoder.decode(kv[1], "UTF-8"); + String value = URLDecoder.decode(kv[1], Request.UTF_8); try { return Long.parseLong(value) * 1000L; } catch (NumberFormatException ignored) { diff --git a/src/test/java/com/soundcloud/api/CloudAPIIntegrationTest.java b/src/test/java/com/soundcloud/api/CloudAPIIntegrationTest.java index 6396403..9de286d 100644 --- a/src/test/java/com/soundcloud/api/CloudAPIIntegrationTest.java +++ b/src/test/java/com/soundcloud/api/CloudAPIIntegrationTest.java @@ -136,7 +136,7 @@ public void shouldCreateAPlaylistAndAddTracksToItWithJSON() throws Exception { String playlistUrl = location.getValue(); assertNotNull(playlistUrl); - String title = "a new title:" + System.currentTimeMillis(); + String title = "a new tîtle:" + System.currentTimeMillis(); JSONObject json = createJSONPlaylist(title, CHE_FLUTE_TRACK_ID, FLICKERMOOD_TRACK_ID); resp = api.put(Request.to(playlistUrl) diff --git a/src/test/java/com/soundcloud/api/RequestTest.java b/src/test/java/com/soundcloud/api/RequestTest.java index ff4acbd..f19128a 100644 --- a/src/test/java/com/soundcloud/api/RequestTest.java +++ b/src/test/java/com/soundcloud/api/RequestTest.java @@ -270,6 +270,19 @@ public void shouldIncludeContentInRequest() throws Exception { assertThat("content", equalTo(body)); } + @Test + public void shouldUseUTF8AsDefaultEncodingForStringPayloads() throws Exception { + HttpPost request = Request.to("/too") + .withContent("{ string:\"îøüöéí\" }", "application/json") + .buildRequest(HttpPost.class); + + ByteArrayOutputStream os = new ByteArrayOutputStream(); + request.getEntity().writeTo(os); + + String decoded = os.toString("UTF-8"); + assertThat("{ string:\"îøüöéí\" }", equalTo(decoded)); + } + @Test public void shouldBuildARequestWithContentAndPreserveQueryParameters() throws Exception { HttpPost post = Request From 3cd91319d10a35096b18a425e12933416ab1e54e Mon Sep 17 00:00:00 2001 From: Jon Schmidt Date: Thu, 18 Apr 2013 19:49:37 +0200 Subject: [PATCH 124/156] removed blank space --- src/main/java/com/soundcloud/api/ApiWrapper.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/com/soundcloud/api/ApiWrapper.java b/src/main/java/com/soundcloud/api/ApiWrapper.java index 90cae3e..9420a8c 100644 --- a/src/main/java/com/soundcloud/api/ApiWrapper.java +++ b/src/main/java/com/soundcloud/api/ApiWrapper.java @@ -607,7 +607,7 @@ protected HttpResponse execute(Request req, Class req } } logRequest(reqType, req); - return execute(addClientIdIfNecessary(req).buildRequest(reqType)); + return execute(addClientIdIfNecessary(req).buildRequest(reqType)); } protected Request addClientIdIfNecessary(Request req) { From 1dd5fc0c6eb6e4e88038dc86fd17bf79c69fcf55 Mon Sep 17 00:00:00 2001 From: Jon Schmidt Date: Thu, 18 Apr 2013 19:52:45 +0200 Subject: [PATCH 125/156] split client id tests --- src/test/java/com/soundcloud/api/ApiWrapperTest.java | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/test/java/com/soundcloud/api/ApiWrapperTest.java b/src/test/java/com/soundcloud/api/ApiWrapperTest.java index 462eeb4..ddc8e1f 100644 --- a/src/test/java/com/soundcloud/api/ApiWrapperTest.java +++ b/src/test/java/com/soundcloud/api/ApiWrapperTest.java @@ -519,8 +519,12 @@ public HttpClient getHttpClient() { } @Test - public void testOnlyAddClientIdWithoutToken() throws Exception { + public void testAddClientIdWithoutToken() throws Exception { assertThat(api.addClientIdIfNecessary(Request.to("/foo")).toUrl(), equalTo("/foo?client_id=" + TEST_CLIENT_ID)); + } + + @Test + public void testDontAddClientIdWithToken() throws Exception { api.setToken(new Token("access", "refresh")); assertThat(api.addClientIdIfNecessary(Request.to("/foo")).toUrl(), equalTo("/foo")); } From cf013227b770a130d33ff4a942cc3929b3441a36 Mon Sep 17 00:00:00 2001 From: Jon Schmidt Date: Thu, 18 Apr 2013 21:23:45 +0200 Subject: [PATCH 126/156] Added constants for oauth Parameters --- .../java/com/soundcloud/api/ApiWrapper.java | 68 +++++++++++-------- 1 file changed, 40 insertions(+), 28 deletions(-) diff --git a/src/main/java/com/soundcloud/api/ApiWrapper.java b/src/main/java/com/soundcloud/api/ApiWrapper.java index 9420a8c..7c8ba7d 100644 --- a/src/main/java/com/soundcloud/api/ApiWrapper.java +++ b/src/main/java/com/soundcloud/api/ApiWrapper.java @@ -91,6 +91,18 @@ public class ApiWrapper implements CloudAPI, Serializable { private static final long serialVersionUID = 3662083416905771921L; private static final Token EMPTY_TOKEN = new Token(null, null); + private static interface AuthParams { + String GRANT_TYPE = "grant_type"; + String CLIENT_ID = "client_id"; + String CLIENT_SECRET = "client_secret"; + String USERNAME = "username"; + String PASSWORD = "password"; + String REDIRECT_URI = "redirect_uri"; + String CODE = "code"; + String REFRESH_TOKEN = "refresh_token"; + String RESPONSE_TYPE = "response_type"; + } + /** The current environment, only live possible for now */ public final Env env = Env.LIVE; @@ -139,11 +151,11 @@ public ApiWrapper(String clientId, throw new IllegalArgumentException("username or password is null"); } final Request request = addScope(Request.to(Endpoints.TOKEN).with( - "grant_type", PASSWORD, - "client_id", mClientId, - "client_secret", mClientSecret, - "username", username, - "password", password), scopes); + AuthParams.GRANT_TYPE, CloudAPI.PASSWORD, + AuthParams.CLIENT_ID, mClientId, + AuthParams.CLIENT_SECRET, mClientSecret, + AuthParams.USERNAME, username, + AuthParams.PASSWORD, password), scopes); mToken = requestToken(request); return mToken; } @@ -155,11 +167,11 @@ public ApiWrapper(String clientId, throw new IllegalArgumentException("code is null"); } final Request request = addScope(Request.to(Endpoints.TOKEN).with( - "grant_type", AUTHORIZATION_CODE, - "client_id", mClientId, - "client_secret", mClientSecret, - "redirect_uri", mRedirectUri, - "code", code), scopes); + AuthParams.GRANT_TYPE, AUTHORIZATION_CODE, + AuthParams.CLIENT_ID, mClientId, + AuthParams.CLIENT_SECRET, mClientSecret, + AuthParams.REDIRECT_URI, mRedirectUri, + AuthParams.CODE, code), scopes); mToken = requestToken(request); return mToken; } @@ -167,9 +179,9 @@ public ApiWrapper(String clientId, @Override public Token clientCredentials(String... scopes) throws IOException { final Request req = addScope(Request.to(Endpoints.TOKEN).with( - "grant_type", CLIENT_CREDENTIALS, - "client_id", mClientId, - "client_secret", mClientSecret), scopes); + AuthParams.GRANT_TYPE, CLIENT_CREDENTIALS, + AuthParams.CLIENT_ID, mClientId, + AuthParams.CLIENT_SECRET, mClientSecret), scopes); final Token token = requestToken(req); if (scopes != null) { @@ -186,9 +198,9 @@ public ApiWrapper(String clientId, @Override public Token extensionGrantType(String grantType, String... scopes) throws IOException { final Request req = addScope(Request.to(Endpoints.TOKEN).with( - "grant_type", grantType, - "client_id", mClientId, - "client_secret", mClientSecret), scopes); + AuthParams.GRANT_TYPE, grantType, + AuthParams.CLIENT_ID, mClientId, + AuthParams.CLIENT_SECRET, mClientSecret), scopes); mToken = requestToken(req); return mToken; @@ -197,20 +209,20 @@ public Token extensionGrantType(String grantType, String... scopes) throws IOExc @Override public Token refreshToken() throws IOException { if (mToken == null || mToken.refresh == null) throw new IllegalStateException("no refresh token available"); mToken = requestToken(Request.to(Endpoints.TOKEN).with( - "grant_type", REFRESH_TOKEN, - "client_id", mClientId, - "client_secret", mClientSecret, - "refresh_token", mToken.refresh)); + AuthParams.GRANT_TYPE, CloudAPI.REFRESH_TOKEN, + AuthParams.CLIENT_ID, mClientId, + AuthParams.CLIENT_SECRET, mClientSecret, + AuthParams.REFRESH_TOKEN, mToken.refresh)); return mToken; } @Override public Token exchangeOAuth1Token(String oauth1AccessToken) throws IOException { if (oauth1AccessToken == null) throw new IllegalArgumentException("need access token"); mToken = requestToken(Request.to(Endpoints.TOKEN).with( - "grant_type", OAUTH1_TOKEN, - "client_id", mClientId, - "client_secret", mClientSecret, - "refresh_token", oauth1AccessToken)); + AuthParams.GRANT_TYPE, OAUTH1_TOKEN, + AuthParams.CLIENT_ID, mClientId, + AuthParams.CLIENT_SECRET, mClientSecret, + AuthParams.REFRESH_TOKEN, oauth1AccessToken)); return mToken; } @@ -231,9 +243,9 @@ public Token extensionGrantType(String grantType, String... scopes) throws IOExc @Override public URI authorizationCodeUrl(String... options) { final Request req = Request.to(options.length == 0 ? Endpoints.CONNECT : options[0]).with( - "redirect_uri", mRedirectUri, - "client_id", mClientId, - "response_type", "code"); + AuthParams.REDIRECT_URI, mRedirectUri, + AuthParams.CLIENT_ID, mClientId, + AuthParams.RESPONSE_TYPE, "code"); if (options.length == 2) req.add("scope", options[1]); return getURI(req, false, true); } @@ -611,7 +623,7 @@ protected HttpResponse execute(Request req, Class req } protected Request addClientIdIfNecessary(Request req) { - return (mToken != EMPTY_TOKEN) ? req : new Request(req).add("client_id", mClientId); + return (mToken != EMPTY_TOKEN) ? req : new Request(req).add(AuthParams.CLIENT_ID, mClientId); } protected void logRequest( Class reqType, Request request) { From c6497ae7904220f01948638b3ddc6be797170916 Mon Sep 17 00:00:00 2001 From: jonschmidt Date: Fri, 19 Apr 2013 12:26:09 +0200 Subject: [PATCH 127/156] Move authparams interface up and static import --- .../java/com/soundcloud/api/ApiWrapper.java | 64 ++++++++----------- .../java/com/soundcloud/api/AuthParams.java | 16 +++++ 2 files changed, 43 insertions(+), 37 deletions(-) create mode 100644 src/main/java/com/soundcloud/api/AuthParams.java diff --git a/src/main/java/com/soundcloud/api/ApiWrapper.java b/src/main/java/com/soundcloud/api/ApiWrapper.java index 7c8ba7d..47e5672 100644 --- a/src/main/java/com/soundcloud/api/ApiWrapper.java +++ b/src/main/java/com/soundcloud/api/ApiWrapper.java @@ -1,5 +1,7 @@ package com.soundcloud.api; +import static com.soundcloud.api.AuthParams.*; + import org.apache.http.ConnectionReuseStrategy; import org.apache.http.Header; import org.apache.http.HeaderElement; @@ -91,18 +93,6 @@ public class ApiWrapper implements CloudAPI, Serializable { private static final long serialVersionUID = 3662083416905771921L; private static final Token EMPTY_TOKEN = new Token(null, null); - private static interface AuthParams { - String GRANT_TYPE = "grant_type"; - String CLIENT_ID = "client_id"; - String CLIENT_SECRET = "client_secret"; - String USERNAME = "username"; - String PASSWORD = "password"; - String REDIRECT_URI = "redirect_uri"; - String CODE = "code"; - String REFRESH_TOKEN = "refresh_token"; - String RESPONSE_TYPE = "response_type"; - } - /** The current environment, only live possible for now */ public final Env env = Env.LIVE; @@ -151,10 +141,10 @@ public ApiWrapper(String clientId, throw new IllegalArgumentException("username or password is null"); } final Request request = addScope(Request.to(Endpoints.TOKEN).with( - AuthParams.GRANT_TYPE, CloudAPI.PASSWORD, - AuthParams.CLIENT_ID, mClientId, - AuthParams.CLIENT_SECRET, mClientSecret, - AuthParams.USERNAME, username, + GRANT_TYPE, CloudAPI.PASSWORD, + CLIENT_ID, mClientId, + CLIENT_SECRET, mClientSecret, + USERNAME, username, AuthParams.PASSWORD, password), scopes); mToken = requestToken(request); return mToken; @@ -167,11 +157,11 @@ public ApiWrapper(String clientId, throw new IllegalArgumentException("code is null"); } final Request request = addScope(Request.to(Endpoints.TOKEN).with( - AuthParams.GRANT_TYPE, AUTHORIZATION_CODE, - AuthParams.CLIENT_ID, mClientId, - AuthParams.CLIENT_SECRET, mClientSecret, - AuthParams.REDIRECT_URI, mRedirectUri, - AuthParams.CODE, code), scopes); + GRANT_TYPE, AUTHORIZATION_CODE, + CLIENT_ID, mClientId, + CLIENT_SECRET, mClientSecret, + REDIRECT_URI, mRedirectUri, + CODE, code), scopes); mToken = requestToken(request); return mToken; } @@ -179,9 +169,9 @@ public ApiWrapper(String clientId, @Override public Token clientCredentials(String... scopes) throws IOException { final Request req = addScope(Request.to(Endpoints.TOKEN).with( - AuthParams.GRANT_TYPE, CLIENT_CREDENTIALS, - AuthParams.CLIENT_ID, mClientId, - AuthParams.CLIENT_SECRET, mClientSecret), scopes); + GRANT_TYPE, CLIENT_CREDENTIALS, + CLIENT_ID, mClientId, + CLIENT_SECRET, mClientSecret), scopes); final Token token = requestToken(req); if (scopes != null) { @@ -198,9 +188,9 @@ public ApiWrapper(String clientId, @Override public Token extensionGrantType(String grantType, String... scopes) throws IOException { final Request req = addScope(Request.to(Endpoints.TOKEN).with( - AuthParams.GRANT_TYPE, grantType, - AuthParams.CLIENT_ID, mClientId, - AuthParams.CLIENT_SECRET, mClientSecret), scopes); + GRANT_TYPE, grantType, + CLIENT_ID, mClientId, + CLIENT_SECRET, mClientSecret), scopes); mToken = requestToken(req); return mToken; @@ -209,9 +199,9 @@ public Token extensionGrantType(String grantType, String... scopes) throws IOExc @Override public Token refreshToken() throws IOException { if (mToken == null || mToken.refresh == null) throw new IllegalStateException("no refresh token available"); mToken = requestToken(Request.to(Endpoints.TOKEN).with( - AuthParams.GRANT_TYPE, CloudAPI.REFRESH_TOKEN, - AuthParams.CLIENT_ID, mClientId, - AuthParams.CLIENT_SECRET, mClientSecret, + GRANT_TYPE, CloudAPI.REFRESH_TOKEN, + CLIENT_ID, mClientId, + CLIENT_SECRET, mClientSecret, AuthParams.REFRESH_TOKEN, mToken.refresh)); return mToken; } @@ -219,9 +209,9 @@ public Token extensionGrantType(String grantType, String... scopes) throws IOExc @Override public Token exchangeOAuth1Token(String oauth1AccessToken) throws IOException { if (oauth1AccessToken == null) throw new IllegalArgumentException("need access token"); mToken = requestToken(Request.to(Endpoints.TOKEN).with( - AuthParams.GRANT_TYPE, OAUTH1_TOKEN, - AuthParams.CLIENT_ID, mClientId, - AuthParams.CLIENT_SECRET, mClientSecret, + GRANT_TYPE, OAUTH1_TOKEN, + CLIENT_ID, mClientId, + CLIENT_SECRET, mClientSecret, AuthParams.REFRESH_TOKEN, oauth1AccessToken)); return mToken; } @@ -243,9 +233,9 @@ public Token extensionGrantType(String grantType, String... scopes) throws IOExc @Override public URI authorizationCodeUrl(String... options) { final Request req = Request.to(options.length == 0 ? Endpoints.CONNECT : options[0]).with( - AuthParams.REDIRECT_URI, mRedirectUri, - AuthParams.CLIENT_ID, mClientId, - AuthParams.RESPONSE_TYPE, "code"); + REDIRECT_URI, mRedirectUri, + CLIENT_ID, mClientId, + RESPONSE_TYPE, "code"); if (options.length == 2) req.add("scope", options[1]); return getURI(req, false, true); } @@ -623,7 +613,7 @@ protected HttpResponse execute(Request req, Class req } protected Request addClientIdIfNecessary(Request req) { - return (mToken != EMPTY_TOKEN) ? req : new Request(req).add(AuthParams.CLIENT_ID, mClientId); + return (mToken != EMPTY_TOKEN) ? req : new Request(req).add(CLIENT_ID, mClientId); } protected void logRequest( Class reqType, Request request) { diff --git a/src/main/java/com/soundcloud/api/AuthParams.java b/src/main/java/com/soundcloud/api/AuthParams.java new file mode 100644 index 0000000..9b33fa5 --- /dev/null +++ b/src/main/java/com/soundcloud/api/AuthParams.java @@ -0,0 +1,16 @@ +package com.soundcloud.api; + +/** + * OAuth2 parameters + */ +public interface AuthParams { + String GRANT_TYPE = "grant_type"; + String CLIENT_ID = "client_id"; + String CLIENT_SECRET = "client_secret"; + String USERNAME = "username"; + String PASSWORD = "password"; + String REDIRECT_URI = "redirect_uri"; + String CODE = "code"; + String REFRESH_TOKEN = "refresh_token"; + String RESPONSE_TYPE = "response_type"; +} From 74bf80277f057ad4a16af870d0a2f55438782920 Mon Sep 17 00:00:00 2001 From: jonschmidt Date: Fri, 19 Apr 2013 14:40:17 +0200 Subject: [PATCH 128/156] remove manual client id to make integration test useful again --- src/test/java/com/soundcloud/api/CloudAPIIntegrationTest.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/test/java/com/soundcloud/api/CloudAPIIntegrationTest.java b/src/test/java/com/soundcloud/api/CloudAPIIntegrationTest.java index 6396403..b7f0c31 100644 --- a/src/test/java/com/soundcloud/api/CloudAPIIntegrationTest.java +++ b/src/test/java/com/soundcloud/api/CloudAPIIntegrationTest.java @@ -70,7 +70,7 @@ private Token login(String... scopes) throws IOException { @Test public void shouldBeAbleToMakePublicRequests() throws Exception { - HttpResponse response = api.get(Request.to("/tracks").with("client_id", CLIENT_ID, "order", "hotness")); + HttpResponse response = api.get(Request.to("/tracks").with("order", "hotness")); assertEquals(200, response.getStatusLine().getStatusCode()); } From 2bd1b59fc918d1b671de41ec10195922106cb14c Mon Sep 17 00:00:00 2001 From: Jon Schmidt Date: Mon, 22 Apr 2013 08:59:03 +0200 Subject: [PATCH 129/156] do not add client id if already present --- src/main/java/com/soundcloud/api/ApiWrapper.java | 3 ++- src/test/java/com/soundcloud/api/ApiWrapperTest.java | 6 ++++++ 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/src/main/java/com/soundcloud/api/ApiWrapper.java b/src/main/java/com/soundcloud/api/ApiWrapper.java index 47e5672..70df45c 100644 --- a/src/main/java/com/soundcloud/api/ApiWrapper.java +++ b/src/main/java/com/soundcloud/api/ApiWrapper.java @@ -613,7 +613,8 @@ protected HttpResponse execute(Request req, Class req } protected Request addClientIdIfNecessary(Request req) { - return (mToken != EMPTY_TOKEN) ? req : new Request(req).add(CLIENT_ID, mClientId); + return (mToken != EMPTY_TOKEN || req.getParams().containsKey(CLIENT_ID)) ? + req : new Request(req).add(CLIENT_ID, mClientId); } protected void logRequest( Class reqType, Request request) { diff --git a/src/test/java/com/soundcloud/api/ApiWrapperTest.java b/src/test/java/com/soundcloud/api/ApiWrapperTest.java index ddc8e1f..1162159 100644 --- a/src/test/java/com/soundcloud/api/ApiWrapperTest.java +++ b/src/test/java/com/soundcloud/api/ApiWrapperTest.java @@ -529,6 +529,12 @@ public void testDontAddClientIdWithToken() throws Exception { assertThat(api.addClientIdIfNecessary(Request.to("/foo")).toUrl(), equalTo("/foo")); } + @Test + public void testDontAddClientIdIfManuallyAdded() throws Exception { + final Request req = Request.to("/foo").with("client_id", "12345"); + assertThat(api.addClientIdIfNecessary(req).toUrl(), equalTo("/foo?client_id=12345")); + } + @Test public void testAddDefaultParameters() throws Exception { layer.addHttpResponseRule("/foo?client_id=" + TEST_CLIENT_ID, "Hi"); From e19c3f8b13499a24e9b00b867b7bdd52beedbe61 Mon Sep 17 00:00:00 2001 From: jonschmidt Date: Mon, 22 Apr 2013 12:34:44 +0200 Subject: [PATCH 130/156] fixed imports --- .../soundcloud/api/examples/FacebookConnect.java | 1 - .../com/soundcloud/api/examples/GetResource.java | 3 --- .../com/soundcloud/api/examples/PostResource.java | 3 --- .../com/soundcloud/api/examples/PutResource.java | 3 --- src/main/java/com/soundcloud/api/ApiWrapper.java | 8 +++++++- .../soundcloud/api/CountingMultipartEntity.java | 6 +++--- .../java/com/soundcloud/api/ApiWrapperTest.java | 13 ++++++++++--- .../soundcloud/api/CloudAPIIntegrationTest.java | 10 ++++++++-- .../api/OAuth2HttpRequestInterceptorTest.java | 4 +++- .../java/com/soundcloud/api/OAuth2SchemeTest.java | 5 ++++- src/test/java/com/soundcloud/api/RequestTest.java | 14 ++++++++++---- src/test/java/com/soundcloud/api/StreamTest.java | 1 - src/test/java/com/soundcloud/api/TokenTest.java | 7 ++++++- 13 files changed, 51 insertions(+), 27 deletions(-) diff --git a/src/examples/java/com/soundcloud/api/examples/FacebookConnect.java b/src/examples/java/com/soundcloud/api/examples/FacebookConnect.java index 5b5f3a1..7368e79 100644 --- a/src/examples/java/com/soundcloud/api/examples/FacebookConnect.java +++ b/src/examples/java/com/soundcloud/api/examples/FacebookConnect.java @@ -3,7 +3,6 @@ import com.soundcloud.api.ApiWrapper; import com.soundcloud.api.CloudAPI; import com.soundcloud.api.Endpoints; -import com.soundcloud.api.Env; import com.soundcloud.api.Token; import java.awt.*; diff --git a/src/examples/java/com/soundcloud/api/examples/GetResource.java b/src/examples/java/com/soundcloud/api/examples/GetResource.java index 056c2f0..ac1d5a3 100644 --- a/src/examples/java/com/soundcloud/api/examples/GetResource.java +++ b/src/examples/java/com/soundcloud/api/examples/GetResource.java @@ -6,9 +6,6 @@ import com.soundcloud.api.Request; import org.apache.http.HttpResponse; import org.apache.http.HttpStatus; -import org.json.JSONArray; -import org.json.JSONException; -import org.json.JSONObject; import java.io.File; diff --git a/src/examples/java/com/soundcloud/api/examples/PostResource.java b/src/examples/java/com/soundcloud/api/examples/PostResource.java index a5bb634..0727708 100644 --- a/src/examples/java/com/soundcloud/api/examples/PostResource.java +++ b/src/examples/java/com/soundcloud/api/examples/PostResource.java @@ -5,9 +5,6 @@ import com.soundcloud.api.Request; import org.apache.http.HttpResponse; import org.apache.http.HttpStatus; -import org.json.JSONArray; -import org.json.JSONException; -import org.json.JSONObject; import java.io.File; diff --git a/src/examples/java/com/soundcloud/api/examples/PutResource.java b/src/examples/java/com/soundcloud/api/examples/PutResource.java index 65a2163..f3faf81 100644 --- a/src/examples/java/com/soundcloud/api/examples/PutResource.java +++ b/src/examples/java/com/soundcloud/api/examples/PutResource.java @@ -6,9 +6,6 @@ import com.soundcloud.api.Request; import org.apache.http.HttpResponse; import org.apache.http.HttpStatus; -import org.json.JSONArray; -import org.json.JSONException; -import org.json.JSONObject; import java.io.File; diff --git a/src/main/java/com/soundcloud/api/ApiWrapper.java b/src/main/java/com/soundcloud/api/ApiWrapper.java index 70df45c..38dee79 100644 --- a/src/main/java/com/soundcloud/api/ApiWrapper.java +++ b/src/main/java/com/soundcloud/api/ApiWrapper.java @@ -1,6 +1,12 @@ package com.soundcloud.api; -import static com.soundcloud.api.AuthParams.*; +import static com.soundcloud.api.AuthParams.CLIENT_ID; +import static com.soundcloud.api.AuthParams.CLIENT_SECRET; +import static com.soundcloud.api.AuthParams.CODE; +import static com.soundcloud.api.AuthParams.GRANT_TYPE; +import static com.soundcloud.api.AuthParams.REDIRECT_URI; +import static com.soundcloud.api.AuthParams.RESPONSE_TYPE; +import static com.soundcloud.api.AuthParams.USERNAME; import org.apache.http.ConnectionReuseStrategy; import org.apache.http.Header; diff --git a/src/main/java/com/soundcloud/api/CountingMultipartEntity.java b/src/main/java/com/soundcloud/api/CountingMultipartEntity.java index 64eff78..fdb8d55 100644 --- a/src/main/java/com/soundcloud/api/CountingMultipartEntity.java +++ b/src/main/java/com/soundcloud/api/CountingMultipartEntity.java @@ -1,13 +1,13 @@ package com.soundcloud.api; +import org.apache.http.Header; +import org.apache.http.HttpEntity; + import java.io.FilterOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; -import org.apache.http.Header; -import org.apache.http.HttpEntity; - class CountingMultipartEntity implements HttpEntity { private HttpEntity mDelegate; private Request.TransferProgressListener mListener; diff --git a/src/test/java/com/soundcloud/api/ApiWrapperTest.java b/src/test/java/com/soundcloud/api/ApiWrapperTest.java index 1162159..8ede1e6 100644 --- a/src/test/java/com/soundcloud/api/ApiWrapperTest.java +++ b/src/test/java/com/soundcloud/api/ApiWrapperTest.java @@ -3,14 +3,21 @@ import static junit.framework.Assert.assertEquals; import static junit.framework.Assert.assertFalse; import static junit.framework.Assert.fail; -import static org.hamcrest.CoreMatchers.*; +import static org.hamcrest.CoreMatchers.containsString; +import static org.hamcrest.CoreMatchers.equalTo; +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.CoreMatchers.nullValue; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertNull; -import static org.junit.Assert.*; +import static org.junit.Assert.assertThat; import static org.junit.Assert.assertTrue; import static org.mockito.Matchers.any; import static org.mockito.Matchers.anyString; -import static org.mockito.Mockito.*; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.reset; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; import com.soundcloud.api.fakehttp.FakeHttpLayer; import com.soundcloud.api.fakehttp.RequestMatcher; diff --git a/src/test/java/com/soundcloud/api/CloudAPIIntegrationTest.java b/src/test/java/com/soundcloud/api/CloudAPIIntegrationTest.java index b7f0c31..fc244a9 100644 --- a/src/test/java/com/soundcloud/api/CloudAPIIntegrationTest.java +++ b/src/test/java/com/soundcloud/api/CloudAPIIntegrationTest.java @@ -3,8 +3,14 @@ import static junit.framework.Assert.assertEquals; import static junit.framework.Assert.assertNotNull; import static junit.framework.Assert.assertTrue; -import static org.hamcrest.CoreMatchers.*; -import static org.junit.Assert.*; +import static org.hamcrest.CoreMatchers.containsString; +import static org.hamcrest.CoreMatchers.equalTo; +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.CoreMatchers.not; +import static org.hamcrest.CoreMatchers.notNullValue; +import static org.hamcrest.CoreMatchers.nullValue; +import static org.junit.Assert.assertThat; +import static org.junit.Assert.fail; import org.apache.http.Header; import org.apache.http.HttpResponse; diff --git a/src/test/java/com/soundcloud/api/OAuth2HttpRequestInterceptorTest.java b/src/test/java/com/soundcloud/api/OAuth2HttpRequestInterceptorTest.java index 7baf4a5..eb0034f 100644 --- a/src/test/java/com/soundcloud/api/OAuth2HttpRequestInterceptorTest.java +++ b/src/test/java/com/soundcloud/api/OAuth2HttpRequestInterceptorTest.java @@ -1,6 +1,8 @@ package com.soundcloud.api; -import static org.mockito.Mockito.*; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; import org.apache.http.Header; import org.apache.http.HttpRequest; diff --git a/src/test/java/com/soundcloud/api/OAuth2SchemeTest.java b/src/test/java/com/soundcloud/api/OAuth2SchemeTest.java index b3588ca..57b878f 100644 --- a/src/test/java/com/soundcloud/api/OAuth2SchemeTest.java +++ b/src/test/java/com/soundcloud/api/OAuth2SchemeTest.java @@ -5,7 +5,10 @@ import static org.hamcrest.CoreMatchers.nullValue; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertThat; -import static org.mockito.Mockito.*; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; import org.apache.http.Header; import org.apache.http.auth.AUTH; diff --git a/src/test/java/com/soundcloud/api/RequestTest.java b/src/test/java/com/soundcloud/api/RequestTest.java index ff4acbd..5f1630f 100644 --- a/src/test/java/com/soundcloud/api/RequestTest.java +++ b/src/test/java/com/soundcloud/api/RequestTest.java @@ -1,9 +1,16 @@ package com.soundcloud.api; -import static org.hamcrest.CoreMatchers.*; -import static org.junit.Assert.*; -import static org.mockito.Matchers.notNull; +import static org.hamcrest.CoreMatchers.containsString; +import static org.hamcrest.CoreMatchers.equalTo; +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.CoreMatchers.not; +import static org.hamcrest.CoreMatchers.notNullValue; +import static org.hamcrest.CoreMatchers.sameInstance; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertThat; +import static org.junit.Assert.assertTrue; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; @@ -18,7 +25,6 @@ import org.apache.http.entity.mime.MultipartEntity; import org.apache.http.util.EntityUtils; import org.hamcrest.CoreMatchers; -import org.junit.Ignore; import org.junit.Test; import java.io.ByteArrayInputStream; diff --git a/src/test/java/com/soundcloud/api/StreamTest.java b/src/test/java/com/soundcloud/api/StreamTest.java index 1650aa1..36f808d 100644 --- a/src/test/java/com/soundcloud/api/StreamTest.java +++ b/src/test/java/com/soundcloud/api/StreamTest.java @@ -4,7 +4,6 @@ import static org.hamcrest.CoreMatchers.is; import static org.hamcrest.CoreMatchers.not; import static org.junit.Assert.assertThat; -import static org.junit.Assert.assertTrue; import org.apache.http.HttpException; import org.apache.http.HttpResponse; diff --git a/src/test/java/com/soundcloud/api/TokenTest.java b/src/test/java/com/soundcloud/api/TokenTest.java index 49a9b07..a053382 100644 --- a/src/test/java/com/soundcloud/api/TokenTest.java +++ b/src/test/java/com/soundcloud/api/TokenTest.java @@ -2,7 +2,12 @@ import static org.hamcrest.CoreMatchers.equalTo; import static org.hamcrest.CoreMatchers.is; -import static org.junit.Assert.*; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertThat; +import static org.junit.Assert.assertTrue; import org.json.JSONObject; import org.junit.Test; From 3b1ece8b0f4790ce65d5ab155faf14c99be6b106 Mon Sep 17 00:00:00 2001 From: jonschmidt Date: Mon, 22 Apr 2013 12:35:35 +0200 Subject: [PATCH 131/156] final on constants --- src/test/java/com/soundcloud/api/ApiWrapperTest.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/test/java/com/soundcloud/api/ApiWrapperTest.java b/src/test/java/com/soundcloud/api/ApiWrapperTest.java index 8ede1e6..c82744a 100644 --- a/src/test/java/com/soundcloud/api/ApiWrapperTest.java +++ b/src/test/java/com/soundcloud/api/ApiWrapperTest.java @@ -53,8 +53,8 @@ public class ApiWrapperTest { private ApiWrapper api; - private static String TEST_CLIENT_ID = "testClientId"; - private static String TEST_CLIENT_SECRET = "testClientSecret"; + private final static String TEST_CLIENT_ID = "testClientId"; + private final static String TEST_CLIENT_SECRET = "testClientSecret"; final FakeHttpLayer layer = new FakeHttpLayer(); @Before public void setup() { From 63ebcc21eb70350198bf08f1c43f2a50311783b6 Mon Sep 17 00:00:00 2001 From: jonschmidt Date: Mon, 22 Apr 2013 14:30:58 +0200 Subject: [PATCH 132/156] consolidated and moved around constants --- .../java/com/soundcloud/api/ApiWrapper.java | 26 +++++++------------ .../java/com/soundcloud/api/AuthParams.java | 16 ------------ .../java/com/soundcloud/api/CloudAPI.java | 19 +++++++++----- src/main/java/com/soundcloud/api/Token.java | 12 ++++----- 4 files changed, 28 insertions(+), 45 deletions(-) delete mode 100644 src/main/java/com/soundcloud/api/AuthParams.java diff --git a/src/main/java/com/soundcloud/api/ApiWrapper.java b/src/main/java/com/soundcloud/api/ApiWrapper.java index 38dee79..408c7de 100644 --- a/src/main/java/com/soundcloud/api/ApiWrapper.java +++ b/src/main/java/com/soundcloud/api/ApiWrapper.java @@ -1,13 +1,5 @@ package com.soundcloud.api; -import static com.soundcloud.api.AuthParams.CLIENT_ID; -import static com.soundcloud.api.AuthParams.CLIENT_SECRET; -import static com.soundcloud.api.AuthParams.CODE; -import static com.soundcloud.api.AuthParams.GRANT_TYPE; -import static com.soundcloud.api.AuthParams.REDIRECT_URI; -import static com.soundcloud.api.AuthParams.RESPONSE_TYPE; -import static com.soundcloud.api.AuthParams.USERNAME; - import org.apache.http.ConnectionReuseStrategy; import org.apache.http.Header; import org.apache.http.HeaderElement; @@ -147,11 +139,11 @@ public ApiWrapper(String clientId, throw new IllegalArgumentException("username or password is null"); } final Request request = addScope(Request.to(Endpoints.TOKEN).with( - GRANT_TYPE, CloudAPI.PASSWORD, + GRANT_TYPE, PASSWORD, CLIENT_ID, mClientId, CLIENT_SECRET, mClientSecret, USERNAME, username, - AuthParams.PASSWORD, password), scopes); + PASSWORD, password), scopes); mToken = requestToken(request); return mToken; } @@ -205,20 +197,20 @@ public Token extensionGrantType(String grantType, String... scopes) throws IOExc @Override public Token refreshToken() throws IOException { if (mToken == null || mToken.refresh == null) throw new IllegalStateException("no refresh token available"); mToken = requestToken(Request.to(Endpoints.TOKEN).with( - GRANT_TYPE, CloudAPI.REFRESH_TOKEN, + GRANT_TYPE, REFRESH_TOKEN, CLIENT_ID, mClientId, CLIENT_SECRET, mClientSecret, - AuthParams.REFRESH_TOKEN, mToken.refresh)); + REFRESH_TOKEN, mToken.refresh)); return mToken; } @Override public Token exchangeOAuth1Token(String oauth1AccessToken) throws IOException { if (oauth1AccessToken == null) throw new IllegalArgumentException("need access token"); mToken = requestToken(Request.to(Endpoints.TOKEN).with( - GRANT_TYPE, OAUTH1_TOKEN, + GRANT_TYPE, OAUTH1_TOKEN_GRANT_TYPE, CLIENT_ID, mClientId, CLIENT_SECRET, mClientSecret, - AuthParams.REFRESH_TOKEN, oauth1AccessToken)); + REFRESH_TOKEN, oauth1AccessToken)); return mToken; } @@ -241,8 +233,8 @@ public Token extensionGrantType(String grantType, String... scopes) throws IOExc final Request req = Request.to(options.length == 0 ? Endpoints.CONNECT : options[0]).with( REDIRECT_URI, mRedirectUri, CLIENT_ID, mClientId, - RESPONSE_TYPE, "code"); - if (options.length == 2) req.add("scope", options[1]); + RESPONSE_TYPE, CODE); + if (options.length == 2) req.add(SCOPE, options[1]); return getURI(req, false, true); } @@ -677,7 +669,7 @@ public void setDefaultAcceptEncoding(String encoding) { scope.append(scopes[i]); if (i < scopes.length-1) scope.append(" "); } - request.add("scope", scope.toString()); + request.add(SCOPE, scope.toString()); } return request; } diff --git a/src/main/java/com/soundcloud/api/AuthParams.java b/src/main/java/com/soundcloud/api/AuthParams.java deleted file mode 100644 index 9b33fa5..0000000 --- a/src/main/java/com/soundcloud/api/AuthParams.java +++ /dev/null @@ -1,16 +0,0 @@ -package com.soundcloud.api; - -/** - * OAuth2 parameters - */ -public interface AuthParams { - String GRANT_TYPE = "grant_type"; - String CLIENT_ID = "client_id"; - String CLIENT_SECRET = "client_secret"; - String USERNAME = "username"; - String PASSWORD = "password"; - String REDIRECT_URI = "redirect_uri"; - String CODE = "code"; - String REFRESH_TOKEN = "refresh_token"; - String RESPONSE_TYPE = "response_type"; -} diff --git a/src/main/java/com/soundcloud/api/CloudAPI.java b/src/main/java/com/soundcloud/api/CloudAPI.java index dcf24be..3adc1ca 100644 --- a/src/main/java/com/soundcloud/api/CloudAPI.java +++ b/src/main/java/com/soundcloud/api/CloudAPI.java @@ -15,17 +15,25 @@ * @see ApiWrapper */ public interface CloudAPI { + // OAuth2 parameters + String GRANT_TYPE = "grant_type"; + String CLIENT_ID = "client_id"; + String CLIENT_SECRET = "client_secret"; + String USERNAME = "username"; + String REDIRECT_URI = "redirect_uri"; + String CODE = "code"; + String RESPONSE_TYPE = "response_type"; + String SCOPE = "scope"; + // standard oauth2 grant types String PASSWORD = "password"; String AUTHORIZATION_CODE = "authorization_code"; String REFRESH_TOKEN = "refresh_token"; String CLIENT_CREDENTIALS = "client_credentials"; - // custom soundcloud - String OAUTH1_TOKEN = "oauth1_token"; - - // oauth2 extension grant types - String FACEBOOK_GRANT_TYPE = "urn:soundcloud:oauth2:grant-type:facebook&access_token="; + // custom + String OAUTH1_TOKEN_GRANT_TYPE = "oauth1_token"; // soundcloud + String FACEBOOK_GRANT_TYPE = "urn:soundcloud:oauth2:grant-type:facebook&access_token="; // oauth2 extension // other constants String REALM = "SoundCloud"; @@ -33,7 +41,6 @@ public interface CloudAPI { String VERSION = "1.2.1"; String USER_AGENT = "SoundCloud Java Wrapper ("+VERSION+")"; - /** * Request a token using * Resource Owner Password Credentials. diff --git a/src/main/java/com/soundcloud/api/Token.java b/src/main/java/com/soundcloud/api/Token.java index 92a5882..1fc6f1f 100644 --- a/src/main/java/com/soundcloud/api/Token.java +++ b/src/main/java/com/soundcloud/api/Token.java @@ -16,20 +16,20 @@ public class Token implements Serializable { private static final long serialVersionUID = 766168501082045382L; - public static final String ACCESS_TOKEN = "access_token"; - public static final String REFRESH_TOKEN = "refresh_token"; - public static final String SCOPE = "scope"; - public static final String EXPIRES_IN = "expires_in"; - public static final String SCOPE_DEFAULT = "*"; /** Special scope for signup / password recovery */ public static final String SCOPE_SIGNUP = "signup"; - public static final String SCOPE_PLAYCOUNT = "playcount"; + public static final String SCOPE_PLAYCOUNT = "playcount"; /** Don't expire access token - returned tokens won't include a refresh token */ public static final String SCOPE_NON_EXPIRING = "non-expiring"; + private static final String ACCESS_TOKEN = "access_token"; + private static final String REFRESH_TOKEN = "refresh_token"; + private static final String SCOPE = "scope"; + private static final String EXPIRES_IN = "expires_in"; + // XXX these should be private public String access, refresh, scope; public long expiresIn; From 58bde44c86a9d036bf66ac60c55a0358caf242fd Mon Sep 17 00:00:00 2001 From: Jan Berkel Date: Mon, 22 Apr 2013 16:28:42 +0200 Subject: [PATCH 133/156] Bump version, prepare 1.3.0 release --- CHANGES.md | 4 +++- README.md | 5 +---- RELEASE.md | 2 +- build.gradle | 2 +- pom.xml | 2 +- src/main/java/com/soundcloud/api/CloudAPI.java | 2 +- src/main/java/com/soundcloud/api/package-info.java | 2 +- 7 files changed, 9 insertions(+), 10 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 59f394f..484bf13 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,5 +1,7 @@ -## 1.2.2 TBD +## 1.3.0 2013-04-22 + * Default to UTF-8 for string payloads + * Include client_id for unauthenticated requests * Support gzip encoding * Fixed non-english request formatting issue * Preserve query parameters for 'withContent` diff --git a/README.md b/README.md index 485c085..0741d42 100644 --- a/README.md +++ b/README.md @@ -126,8 +126,6 @@ The wrapper ships with a few examples in `src/examples/java`: * [FacebookConnect][] obtain an access token via Facebook login You can use gradle tasks to compile and run these examples with one command. -If you don't want to use gradle there is also a precompiled jar with all -dependencies available ([jar-all][]). First create a wrapper and remember to substitute all credentials with real ones ([register an app][register-app] if you need client_id/secret): @@ -254,7 +252,7 @@ See LICENSE for details. [Apache HttpClient]: http://hc.apache.org/httpcomponents-client-ga/ [HttpMime]: http://hc.apache.org/httpcomponents-client-ga/httpmime [json-java]: http://json.org/java/ -[javadoc]: http://soundcloud.github.com/java-api-wrapper/javadoc/1.2.0/com/soundcloud/api/package-summary.html +[javadoc]: http://soundcloud.github.com/java-api-wrapper/javadoc/1.3.0/com/soundcloud/api/package-summary.html [soundcloudapi-java]: http://code.google.com/p/soundcloudapi-java/ [soundcloudapi-java-annouce]: http://blog.soundcloud.com/2010/01/08/java-wrapper/ [CreateWrapper]: https://github.com/soundcloud/java-api-wrapper/blob/master/src/examples/java/com/soundcloud/api/examples/CreateWrapper.java @@ -266,7 +264,6 @@ See LICENSE for details. [SoundCloud Android]: https://play.google.com/store/apps/details?id=com.soundcloud.android [register-app]: http://soundcloud.com/you/apps/new [Apache Maven]: http://maven.apache.org/ -[jar-all]: https://github.com/downloads/soundcloud/java-api-wrapper/java-api-wrapper-1.2.0-all.jar [downloads]: https://github.com/soundcloud/java-api-wrapper/archives/master [snapshots]: https://oss.sonatype.org/content/repositories/snapshots/com/soundcloud/java-api-wrapper/ [releases]: https://oss.sonatype.org/content/repositories/releases/com/soundcloud/java-api-wrapper/ diff --git a/RELEASE.md b/RELEASE.md index 5621018..7280db0 100644 --- a/RELEASE.md +++ b/RELEASE.md @@ -11,7 +11,7 @@ Bump version constants in * build.gradle * src/main/java/com/soundcloud/api/package-info.java * src/main/java/com/soundcloud/api/CloudAPI.java - * README.md (javadoc, jar-all) + * README.md (javadoc) Regenerate + publish javadoc: diff --git a/build.gradle b/build.gradle index dc10f40..2d6f359 100644 --- a/build.gradle +++ b/build.gradle @@ -4,7 +4,7 @@ apply plugin: 'eclipse' apply plugin: 'maven' sourceCompatibility = 1.6 -version = '1.2.2-SNAPSHOT' +version = '1.3.0-SNAPSHOT' group = 'com.soundcloud' repositories { mavenCentral() } diff --git a/pom.xml b/pom.xml index 29c6fd9..eb9e0b2 100644 --- a/pom.xml +++ b/pom.xml @@ -3,7 +3,7 @@ 4.0.0 com.soundcloud java-api-wrapper - 1.2.2-SNAPSHOT + 1.3.0-SNAPSHOT SoundCloud Java API wrapper SoundCloud Java API wrapper (OAuth2 only), works on Android https://github.com/soundcloud/java-api-wrapper#readme diff --git a/src/main/java/com/soundcloud/api/CloudAPI.java b/src/main/java/com/soundcloud/api/CloudAPI.java index 110b628..6468bea 100644 --- a/src/main/java/com/soundcloud/api/CloudAPI.java +++ b/src/main/java/com/soundcloud/api/CloudAPI.java @@ -39,7 +39,7 @@ public interface CloudAPI { // other constants String REALM = "SoundCloud"; String OAUTH_SCHEME = "oauth"; - String VERSION = "1.2.1"; + String VERSION = "1.3.0"; String USER_AGENT = "SoundCloud Java Wrapper ("+VERSION+")"; /** diff --git a/src/main/java/com/soundcloud/api/package-info.java b/src/main/java/com/soundcloud/api/package-info.java index 0db32d4..4ed2a31 100644 --- a/src/main/java/com/soundcloud/api/package-info.java +++ b/src/main/java/com/soundcloud/api/package-info.java @@ -7,6 +7,6 @@ * * @see com.soundcloud.api.ApiWrapper * @author Jan Berkel - * @version 1.2.1, 10/01/12 + * @version 1.3.0, 22/04/13 */ package com.soundcloud.api; From 3d22f00fe9d85afe7abdf04eb07fc1fd5ac53eeb Mon Sep 17 00:00:00 2001 From: Jan Berkel Date: Mon, 22 Apr 2013 16:32:29 +0200 Subject: [PATCH 134/156] Update release instructions --- RELEASE.md | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/RELEASE.md b/RELEASE.md index 7280db0..ea7cdc3 100644 --- a/RELEASE.md +++ b/RELEASE.md @@ -13,11 +13,7 @@ Bump version constants in * src/main/java/com/soundcloud/api/CloudAPI.java * README.md (javadoc) -Regenerate + publish javadoc: - - $ ./update_javadoc.sh - -Regenerate pom.xml +Regenerate pom.xml (only if build.gradle has changed) $ gradle writePom @@ -26,8 +22,8 @@ This doesn't work properly at the moment - use `gradle uploadArchive` and copy ## Releasing to Sonatype OSS (staging) - (make sure there are no uncommitted changes in the repo) - $ mvn -Dresume=false release:prepare # tag repo, bump pom.xml (needs SNAPSHOT tag in pom) + (make sure there are no uncommitted changes in the repo and pom has SNAPSHOT tag) + $ mvn -Dresume=false release:prepare # tag repo, bump pom.xml $ mvn release:perform -Darguments="-Dgpg.keyname=jan@soundcloud.com -Dgpg.passphrase=" This will build and sign all artifcats and upload them to the staging server. @@ -39,6 +35,11 @@ staging repository which can be used for testing. Once everything works you select "Release" to actually release it to the [release repo][]. The release repo is synced with [Maven Central][]. +Regenerate + publish javadoc (no SNAPSHOT tag in build.gradle): + + $ ./update_javadoc.sh + + ## Releasing snapshot versions This is for releasing developer version of the package and can be done anytime, From 31d5d05973906962ea678887f4a235ac03d8afa2 Mon Sep 17 00:00:00 2001 From: Jan Berkel Date: Mon, 22 Apr 2013 16:34:27 +0200 Subject: [PATCH 135/156] [maven-release-plugin] prepare release 1.3.0 --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index eb9e0b2..d5f6a21 100644 --- a/pom.xml +++ b/pom.xml @@ -3,7 +3,7 @@ 4.0.0 com.soundcloud java-api-wrapper - 1.3.0-SNAPSHOT + 1.3.0 SoundCloud Java API wrapper SoundCloud Java API wrapper (OAuth2 only), works on Android https://github.com/soundcloud/java-api-wrapper#readme From c82fd71afe7d3b87503195bca56832394371c15f Mon Sep 17 00:00:00 2001 From: Jan Berkel Date: Mon, 22 Apr 2013 16:34:33 +0200 Subject: [PATCH 136/156] [maven-release-plugin] prepare for next development iteration --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index d5f6a21..3cad005 100644 --- a/pom.xml +++ b/pom.xml @@ -3,7 +3,7 @@ 4.0.0 com.soundcloud java-api-wrapper - 1.3.0 + 1.3.1-SNAPSHOT SoundCloud Java API wrapper SoundCloud Java API wrapper (OAuth2 only), works on Android https://github.com/soundcloud/java-api-wrapper#readme From 5e830db9ebcb03869b3ad146197738f100e84e09 Mon Sep 17 00:00:00 2001 From: Jan Berkel Date: Wed, 5 Jun 2013 16:24:35 +0200 Subject: [PATCH 137/156] Adding support for setting multiple values per query parameter cf http://stackoverflow.com/questions/16929841/posting-to-twitter-and-facebook-using-the-soundcloud-api-on-android --- src/main/java/com/soundcloud/api/Request.java | 28 ++++++++++++++++-- .../java/com/soundcloud/api/RequestTest.java | 29 +++++++++++++++++++ 2 files changed, 54 insertions(+), 3 deletions(-) diff --git a/src/main/java/com/soundcloud/api/Request.java b/src/main/java/com/soundcloud/api/Request.java index e0ee8c9..9afbe1a 100644 --- a/src/main/java/com/soundcloud/api/Request.java +++ b/src/main/java/com/soundcloud/api/Request.java @@ -137,16 +137,38 @@ public static Request to(String resource, Object... args) { } /** - * Adds a key value pair + * Adds a key/value pair. + *
+     * Request r = new Request.to("/foo")
+     *    .add("singleParam", "value")
+     *    .add("multiParam", new String[] { "1", "2", "3" })
+     *    .add("singleParamWithOutValue", null);
+     * 
+ * * @param name the name - * @param value the value + * @param value the value to set, will be obtained via {@link String#valueOf(boolean)}. + * If null, only the parameter is set. + * It can also be a collection or array, in which case all elements are added as query parameters * @return this */ public Request add(String name, Object value) { - mParams.add(new BasicNameValuePair(name, String.valueOf(value))); + if (value instanceof Iterable) { + for (Object o : ((Iterable)value)) { + addParam(name, o); + } + } else if (value instanceof Object[]) { + for (Object o : (Object[])value) { + addParam(name, o); + } + } else { + addParam(name, value); + } return this; } + private void addParam(String name, Object param) { + mParams.add(new BasicNameValuePair(name, param == null ? null : String.valueOf(param))); + } /** * Sets a new parameter, overwriting previous value. diff --git a/src/test/java/com/soundcloud/api/RequestTest.java b/src/test/java/com/soundcloud/api/RequestTest.java index 1a63626..10c1333 100644 --- a/src/test/java/com/soundcloud/api/RequestTest.java +++ b/src/test/java/com/soundcloud/api/RequestTest.java @@ -31,6 +31,7 @@ import java.io.ByteArrayOutputStream; import java.io.File; import java.net.URI; +import java.util.Arrays; import java.util.IllegalFormatException; import java.util.Iterator; import java.util.NoSuchElementException; @@ -95,6 +96,34 @@ public void shouldSupportOverwritingParameters() { assertThat(r.queryString(), equalTo("")); } + @Test + public void shouldAddParameter() throws Exception { + Request r = new Request(); + r.add("param", "value"); + assertThat(r.queryString(), equalTo("param=value")); + } + + @Test + public void shouldAddOnlyParameterNameIfPassedNullValue() throws Exception { + Request r = new Request(); + r.add("param", null); + assertThat(r.queryString(), equalTo("param")); + } + + @Test + public void shouldAddAllContainedValuesIfPassedArrays() throws Exception { + Request r = new Request(); + r.add("foo", new String[] { "1", "2"}); + assertThat(r.queryString(), equalTo("foo=1&foo=2")); + } + + @Test + public void shouldAddAllContainedValuesIfPassedIterable() throws Exception { + Request r = new Request(); + r.add("foo", Arrays.asList("1", "2")); + assertThat(r.queryString(), equalTo("foo=1&foo=2")); + } + @Test public void shouldCopyRequestWithNewResource() throws Exception { Request p = new Request().with("foo", 100, "baz", 22.3f); From 07d379ec89ae8ec5f28b0c732a4b123eba585d0f Mon Sep 17 00:00:00 2001 From: Jan Berkel Date: Wed, 5 Jun 2013 16:41:33 +0200 Subject: [PATCH 138/156] Bump version --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index 2d6f359..e1d2451 100644 --- a/build.gradle +++ b/build.gradle @@ -4,7 +4,7 @@ apply plugin: 'eclipse' apply plugin: 'maven' sourceCompatibility = 1.6 -version = '1.3.0-SNAPSHOT' +version = '1.3.1-SNAPSHOT' group = 'com.soundcloud' repositories { mavenCentral() } From 2437ddae4be70bde842e8d0971b1cb45110a7191 Mon Sep 17 00:00:00 2001 From: jonschmidt Date: Mon, 2 Sep 2013 14:34:39 +0200 Subject: [PATCH 139/156] Do not default to using env.sslResourceHost for resolving streams - this will allow the media.soundcloud.com urls to be resolved for explore --- src/main/java/com/soundcloud/api/ApiWrapper.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/java/com/soundcloud/api/ApiWrapper.java b/src/main/java/com/soundcloud/api/ApiWrapper.java index 408c7de..2ee3557 100644 --- a/src/main/java/com/soundcloud/api/ApiWrapper.java +++ b/src/main/java/com/soundcloud/api/ApiWrapper.java @@ -488,7 +488,7 @@ public long resolve(String url) throws IOException { @Override public Stream resolveStreamUrl(final String url, boolean skipLogging) throws IOException { - HttpResponse resp = head(Request.to(url)); + HttpResponse resp = safeExecute(null, addHeaders(Request.to(url).buildRequest(HttpHead.class))); if (resp.getStatusLine().getStatusCode() == HttpStatus.SC_MOVED_TEMPORARILY) { Header location = resp.getFirstHeader("Location"); if (location != null && location.getValue() != null) { @@ -502,7 +502,7 @@ public Stream resolveStreamUrl(final String url, boolean skipLogging) throws IOE // skip logging req.with("skip_logging", "1"); } - resp = get(req); + resp = safeExecute(null, addHeaders(Request.to(url).buildRequest(HttpGet.class))); if (resp.getStatusLine().getStatusCode() == HttpStatus.SC_MOVED_TEMPORARILY) { return stream.withNewStreamUrl(resp.getFirstHeader("Location").getValue()); } else { From 29bd26daa6e5fb937a39f8b7b2b24cbfcc5d2a41 Mon Sep 17 00:00:00 2001 From: jonschmidt Date: Mon, 2 Sep 2013 19:13:26 +0200 Subject: [PATCH 140/156] test for Non-api stream url resolving added --- .../com/soundcloud/api/CloudAPIIntegrationTest.java | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/src/test/java/com/soundcloud/api/CloudAPIIntegrationTest.java b/src/test/java/com/soundcloud/api/CloudAPIIntegrationTest.java index 4f48a22..6508e71 100644 --- a/src/test/java/com/soundcloud/api/CloudAPIIntegrationTest.java +++ b/src/test/java/com/soundcloud/api/CloudAPIIntegrationTest.java @@ -42,6 +42,7 @@ public class CloudAPIIntegrationTest implements Params.Track, Endpoints { static final String CLIENT_SECRET = "ff3685dbf02ce789a16631b0028e0512"; public static final String TRACK_PERMALINK = "http://soundcloud.com/jberkel/nobody-home"; + public static final String MEDIA_LINK = "http://media.soundcloud.com/stream/zkwlN5MGNsJt"; public static final long USER_ID = 18173653L; public static final long CHE_FLUTE_TRACK_ID = 274334; public static final long FLICKERMOOD_TRACK_ID = 293; @@ -251,6 +252,18 @@ public void shouldResolveStreamUrls() throws Exception { assertThat(resolved.eTag, equalTo("\"980f61d6d6ee26ffe0c78aef618d786f\"")); } + @Test + public void shouldResolveNonApiStreamUrls() throws Exception { + login(); + Stream resolved = api.resolveStreamUrl(MEDIA_LINK, false); + + assertThat(resolved.url, equalTo(MEDIA_LINK)); + assertThat(resolved.streamUrl, containsString("http://ec-media.soundcloud.com/")); + + assertTrue("expire should be in the future", resolved.expires > System.currentTimeMillis()); + assertThat(resolved.eTag, equalTo("\"5eeb63b73f99ff2de44a60441d421d2a\"")); + } + @Test @Ignore /* playcounts not deployed on sandbox */ public void shouldResolveStreamUrlAndSkipPlaycountLogging() throws Exception { // need the playcount scope for this to work From 21115999bbcc5c010dbb9dbc647fbf257a2ed10c Mon Sep 17 00:00:00 2001 From: jonschmidt Date: Tue, 3 Sep 2013 12:14:06 +0200 Subject: [PATCH 141/156] added no login test for resolveStreamUrl --- .../java/com/soundcloud/api/CloudAPIIntegrationTest.java | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/test/java/com/soundcloud/api/CloudAPIIntegrationTest.java b/src/test/java/com/soundcloud/api/CloudAPIIntegrationTest.java index 6508e71..e22b13d 100644 --- a/src/test/java/com/soundcloud/api/CloudAPIIntegrationTest.java +++ b/src/test/java/com/soundcloud/api/CloudAPIIntegrationTest.java @@ -254,7 +254,16 @@ public void shouldResolveStreamUrls() throws Exception { @Test public void shouldResolveNonApiStreamUrls() throws Exception { + testResolveNonApiStreamUrls(); + } + + @Test + public void shouldResolveNonApiStreamUrlsWithLogin() throws Exception { login(); + testResolveNonApiStreamUrls(); + } + + private void testResolveNonApiStreamUrls() throws IOException { Stream resolved = api.resolveStreamUrl(MEDIA_LINK, false); assertThat(resolved.url, equalTo(MEDIA_LINK)); From bf82ba7914420427cba33a6b043374b0f6d68c98 Mon Sep 17 00:00:00 2001 From: Simon Date: Tue, 10 Sep 2013 19:12:15 +0200 Subject: [PATCH 142/156] Update FacebookConnect.java fixed type and added return statement for shouldOverrideUrlLoading --- .../java/com/soundcloud/api/examples/FacebookConnect.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/examples/java/com/soundcloud/api/examples/FacebookConnect.java b/src/examples/java/com/soundcloud/api/examples/FacebookConnect.java index 7368e79..721558d 100644 --- a/src/examples/java/com/soundcloud/api/examples/FacebookConnect.java +++ b/src/examples/java/com/soundcloud/api/examples/FacebookConnect.java @@ -51,7 +51,7 @@ public static void main(String[] args) throws IOException { // start a web server to get the redirect information startServer(wrapper); - // note: on Android you would use a WebView instead an override 'shouldOverrideUrlLoading': + // note: on Android you would use a WebView instead and override 'shouldOverrideUrlLoading': /* WebView webView = (WebView) findViewById(R.id.webview); @@ -63,6 +63,7 @@ public boolean shouldOverrideUrlLoading(final WebView view, String url) { String error = result.getQueryParameter("error"); String code = result.getQueryParameter("code"); } + return true; } }); From ff587d719ad67b37110d366abb5e99b3e35d4875 Mon Sep 17 00:00:00 2001 From: jonschmidt Date: Mon, 16 Sep 2013 12:48:53 +0200 Subject: [PATCH 143/156] Add spam warning code constantto ApiWrapper --- src/main/java/com/soundcloud/api/ApiWrapper.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/main/java/com/soundcloud/api/ApiWrapper.java b/src/main/java/com/soundcloud/api/ApiWrapper.java index 2ee3557..2d27a23 100644 --- a/src/main/java/com/soundcloud/api/ApiWrapper.java +++ b/src/main/java/com/soundcloud/api/ApiWrapper.java @@ -110,6 +110,8 @@ public class ApiWrapper implements CloudAPI, Serializable { public static final long KEEPALIVE_TIMEOUT = 20 * 1000; /* maximum number of connections allowed */ public static final int MAX_TOTAL_CONNECTIONS = 10; + /* spam response code from API */ + public static final int STATUS_CODE_SPAM_WARNING = 429; /** debug request details to stderr */ public boolean debugRequests; From e29d35cd29016b1690d7d598e567ac190bc26994 Mon Sep 17 00:00:00 2001 From: Jan Berkel Date: Wed, 2 Oct 2013 17:20:13 +0200 Subject: [PATCH 144/156] Always add client id to request Closes #23 --- .../java/com/soundcloud/api/ApiWrapper.java | 14 ++++----- .../com/soundcloud/api/ApiWrapperTest.java | 4 +-- .../api/CloudAPIIntegrationTest.java | 30 ------------------- 3 files changed, 9 insertions(+), 39 deletions(-) diff --git a/src/main/java/com/soundcloud/api/ApiWrapper.java b/src/main/java/com/soundcloud/api/ApiWrapper.java index 2d27a23..b564ced 100644 --- a/src/main/java/com/soundcloud/api/ApiWrapper.java +++ b/src/main/java/com/soundcloud/api/ApiWrapper.java @@ -469,16 +469,17 @@ public long resolve(String url) throws IOException { HttpResponse resp = get(Request.to(Endpoints.RESOLVE).with("url", url)); if (resp.getStatusLine().getStatusCode() == HttpStatus.SC_MOVED_TEMPORARILY) { Header location = resp.getFirstHeader("Location"); - if (location != null) { - String s = location.getValue(); - if (s.contains("/")) { + if (location != null && location.getValue() != null) { + final String path = URI.create(location.getValue()).getPath(); + if (path != null && path.contains("/")) { try { - return Integer.parseInt(s.substring(s.lastIndexOf("/") + 1, s.length())); + final String id = path.substring(path.lastIndexOf("/") + 1); + return Integer.parseInt(id); } catch (NumberFormatException e) { throw new ResolverException(e, resp); } } else { - throw new ResolverException("Invalid string:"+s, resp); + throw new ResolverException("Invalid string:"+path, resp); } } else { throw new ResolverException("No location header", resp); @@ -613,8 +614,7 @@ protected HttpResponse execute(Request req, Class req } protected Request addClientIdIfNecessary(Request req) { - return (mToken != EMPTY_TOKEN || req.getParams().containsKey(CLIENT_ID)) ? - req : new Request(req).add(CLIENT_ID, mClientId); + return req.getParams().containsKey(CLIENT_ID) ? req : new Request(req).add(CLIENT_ID, mClientId); } protected void logRequest( Class reqType, Request request) { diff --git a/src/test/java/com/soundcloud/api/ApiWrapperTest.java b/src/test/java/com/soundcloud/api/ApiWrapperTest.java index c82744a..d37ee4b 100644 --- a/src/test/java/com/soundcloud/api/ApiWrapperTest.java +++ b/src/test/java/com/soundcloud/api/ApiWrapperTest.java @@ -531,9 +531,9 @@ public void testAddClientIdWithoutToken() throws Exception { } @Test - public void testDontAddClientIdWithToken() throws Exception { + public void testShouldAlwaysAddClientIdEvenWhenAuthenticated() throws Exception { api.setToken(new Token("access", "refresh")); - assertThat(api.addClientIdIfNecessary(Request.to("/foo")).toUrl(), equalTo("/foo")); + assertThat(api.addClientIdIfNecessary(Request.to("/foo")).toUrl(), equalTo("/foo?client_id=" + TEST_CLIENT_ID)); } @Test diff --git a/src/test/java/com/soundcloud/api/CloudAPIIntegrationTest.java b/src/test/java/com/soundcloud/api/CloudAPIIntegrationTest.java index e22b13d..a75f9ac 100644 --- a/src/test/java/com/soundcloud/api/CloudAPIIntegrationTest.java +++ b/src/test/java/com/soundcloud/api/CloudAPIIntegrationTest.java @@ -273,26 +273,6 @@ private void testResolveNonApiStreamUrls() throws IOException { assertThat(resolved.eTag, equalTo("\"5eeb63b73f99ff2de44a60441d421d2a\"")); } - @Test @Ignore /* playcounts not deployed on sandbox */ - public void shouldResolveStreamUrlAndSkipPlaycountLogging() throws Exception { - // need the playcount scope for this to work - assertTrue(login(Token.SCOPE_PLAYCOUNT).scoped(Token.SCOPE_PLAYCOUNT)); - - long trackId = api.resolve(TRACK_PERMALINK); - - int count = Http.getJSON(api.get(Request.to("/tracks/"+trackId))).getInt("playback_count"); - api.resolveStreamUrl("https://api.soundcloud.com/tracks/"+trackId+"/stream", false); - int count2 = Http.getJSON(api.get(Request.to("/tracks/"+trackId))).getInt("playback_count"); - - assertTrue(String.format("%d !> %d", count2, count), count2 > count); - - // resolve again, this time skipping count - api.resolveStreamUrl("https://api.soundcloud.com/tracks/"+trackId+"/stream", true); - - int count3 = Http.getJSON(api.get(Request.to("/tracks/"+trackId))).getInt("playback_count"); - assertTrue(String.format("%d != %d", count3, count2), count3 == count2); - } - @Test public void shouldThrowResolverExceptionWhenStreamCannotBeResolved() throws Exception { login(); @@ -463,16 +443,6 @@ public void run() { System.err.println("all threads finished"); } - @Test @Ignore - public void updateMyDetails() throws Exception { - Request updateMe = Request.to(MY_DETAILS).with( - Params.User.WEBSITE, "http://mywebsite.com") - .withFile(Params.User.AVATAR, new File(getClass().getResource("cat.jpg").getFile())); - - HttpResponse resp = api.put(updateMe); - assertThat(resp.getStatusLine().getStatusCode(), is(200)); - } - @SuppressWarnings({"UnusedDeclaration"}) private void writeResponse(HttpResponse resp, String file) throws IOException { FileOutputStream fos = new FileOutputStream(file); From d18f6d3ba1f4eada62de154676e3f25e5aed3fe5 Mon Sep 17 00:00:00 2001 From: Jan Berkel Date: Wed, 2 Oct 2013 18:20:29 +0200 Subject: [PATCH 145/156] Update changes --- CHANGES.md | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/CHANGES.md b/CHANGES.md index 484bf13..b891210 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,3 +1,9 @@ +## 1.3.1 2013-10-02 + + * Always include client_id in requests [23] + * Added support for setting multiple values per query parameter [5e830db] + * Do not default to using env.sslResourceHost for resolving streams [21] + ## 1.3.0 2013-04-22 * Default to UTF-8 for string payloads @@ -59,6 +65,9 @@ * Initial release +[5e830db]: https://github.com/soundcloud/java-api-wrapper/commit/5e830db9ebcb03869b3ad146197738f100e84e09 +[23]: https://github.com/soundcloud/java-api-wrapper/issues/23 +[21]: https://github.com/soundcloud/java-api-wrapper/pull/21 [6]: https://github.com/soundcloud/java-api-wrapper/issues/6 [7]: https://github.com/soundcloud/java-api-wrapper/issues/7 [8]: https://github.com/soundcloud/java-api-wrapper/issues/8 From f947bf316d64b61285cbe0285ef23e579bfedecc Mon Sep 17 00:00:00 2001 From: Jan Berkel Date: Wed, 2 Oct 2013 18:37:35 +0200 Subject: [PATCH 146/156] Remove oauth1 migration docs + methods Closes #19 --- README.md | 12 ------------ src/main/java/com/soundcloud/api/ApiWrapper.java | 10 ---------- src/main/java/com/soundcloud/api/CloudAPI.java | 11 ----------- .../java/com/soundcloud/api/ApiWrapperTest.java | 16 ---------------- 4 files changed, 49 deletions(-) diff --git a/README.md b/README.md index 0741d42..8f6f994 100644 --- a/README.md +++ b/README.md @@ -44,18 +44,6 @@ HttpResponse resp = .withFile("user[avatar_data]", new File("flute.jpg"))); ``` -## Migrating from OAuth1 - -If your app uses OAuth1 and already has users with access tokens -you can easily migrate to OAuth2 without requiring anybody to reauthenticate: - -```java -Token token = wrapper.exchangeOAuth1Token("validoauth1token"); -``` - -Note that this is specific to SoundCloud and not part of the current OAuth2 -draft. - ## Refresh tokens OAuth2 access tokens are only valid for a certain amount of time (usually 1h) diff --git a/src/main/java/com/soundcloud/api/ApiWrapper.java b/src/main/java/com/soundcloud/api/ApiWrapper.java index b564ced..8ed000f 100644 --- a/src/main/java/com/soundcloud/api/ApiWrapper.java +++ b/src/main/java/com/soundcloud/api/ApiWrapper.java @@ -206,16 +206,6 @@ public Token extensionGrantType(String grantType, String... scopes) throws IOExc return mToken; } - @Override public Token exchangeOAuth1Token(String oauth1AccessToken) throws IOException { - if (oauth1AccessToken == null) throw new IllegalArgumentException("need access token"); - mToken = requestToken(Request.to(Endpoints.TOKEN).with( - GRANT_TYPE, OAUTH1_TOKEN_GRANT_TYPE, - CLIENT_ID, mClientId, - CLIENT_SECRET, mClientSecret, - REFRESH_TOKEN, oauth1AccessToken)); - return mToken; - } - @Override public Token invalidateToken() { if (mToken != null) { Token alternative = listener == null ? null : listener.onTokenInvalid(mToken); diff --git a/src/main/java/com/soundcloud/api/CloudAPI.java b/src/main/java/com/soundcloud/api/CloudAPI.java index 6468bea..5188e36 100644 --- a/src/main/java/com/soundcloud/api/CloudAPI.java +++ b/src/main/java/com/soundcloud/api/CloudAPI.java @@ -107,17 +107,6 @@ public interface CloudAPI { */ Token refreshToken() throws IOException; - /** - * Exchange an OAuth1 Token for new OAuth2 tokens. The old OAuth1 token will be expired if - * the exchange is successful. - * - * @param oauth1AccessToken a valid OAuth1 access token, registered with the same client - * @return a valid token - * @throws IOException IO/Error - * @throws InvalidTokenException Token error - */ - Token exchangeOAuth1Token(String oauth1AccessToken) throws IOException; - /** * This method should be called when the token was found to be invalid. * Also replaces the current token, if there is one available. diff --git a/src/test/java/com/soundcloud/api/ApiWrapperTest.java b/src/test/java/com/soundcloud/api/ApiWrapperTest.java index d37ee4b..4c050b1 100644 --- a/src/test/java/com/soundcloud/api/ApiWrapperTest.java +++ b/src/test/java/com/soundcloud/api/ApiWrapperTest.java @@ -118,22 +118,6 @@ public void clientCredentialsShouldThrowIfScopeCanNotBeObtained() throws Excepti api.clientCredentials("unlimitedammo"); } - @Test - public void exchangeOAuth1Token() throws Exception { - layer.addPendingHttpResponse(200, "{\n" + - " \"access_token\": \"04u7h-4cc355-70k3n\",\n" + - " \"expires_in\": 3600,\n" + - " \"scope\": \"*\",\n" + - " \"refresh_token\": \"04u7h-r3fr35h-70k3n\"\n" + - "}"); - api.exchangeOAuth1Token("oldtoken"); - } - - @Test(expected = IllegalArgumentException.class) - public void exchangeOAuth1TokenWithEmptyTokenShouldThrow() throws Exception { - api.exchangeOAuth1Token(null); - } - @Test public void shouldGetTokensWhenLoggingIn() throws Exception { layer.addPendingHttpResponse(200, "{\n" + From 0ebf3ef96d60e8469d8edfa378139ce2190507f8 Mon Sep 17 00:00:00 2001 From: Jan Berkel Date: Wed, 2 Oct 2013 18:40:25 +0200 Subject: [PATCH 147/156] changes --- CHANGES.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGES.md b/CHANGES.md index b891210..6ca7c31 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,5 +1,6 @@ ## 1.3.1 2013-10-02 + * Remove oauth1 migration docs + methods [19] * Always include client_id in requests [23] * Added support for setting multiple values per query parameter [5e830db] * Do not default to using env.sslResourceHost for resolving streams [21] @@ -68,6 +69,7 @@ [5e830db]: https://github.com/soundcloud/java-api-wrapper/commit/5e830db9ebcb03869b3ad146197738f100e84e09 [23]: https://github.com/soundcloud/java-api-wrapper/issues/23 [21]: https://github.com/soundcloud/java-api-wrapper/pull/21 +[19]: https://github.com/soundcloud/java-api-wrapper/issues/19 [6]: https://github.com/soundcloud/java-api-wrapper/issues/6 [7]: https://github.com/soundcloud/java-api-wrapper/issues/7 [8]: https://github.com/soundcloud/java-api-wrapper/issues/8 From 9fbcd1f0a43907998ee3b844e015cc65f284cea0 Mon Sep 17 00:00:00 2001 From: Jan Berkel Date: Wed, 2 Oct 2013 18:52:00 +0200 Subject: [PATCH 148/156] prepare release --- README.md | 2 +- src/main/java/com/soundcloud/api/CloudAPI.java | 2 +- src/main/java/com/soundcloud/api/package-info.java | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 8f6f994..bed77aa 100644 --- a/README.md +++ b/README.md @@ -240,7 +240,7 @@ See LICENSE for details. [Apache HttpClient]: http://hc.apache.org/httpcomponents-client-ga/ [HttpMime]: http://hc.apache.org/httpcomponents-client-ga/httpmime [json-java]: http://json.org/java/ -[javadoc]: http://soundcloud.github.com/java-api-wrapper/javadoc/1.3.0/com/soundcloud/api/package-summary.html +[javadoc]: http://soundcloud.github.com/java-api-wrapper/javadoc/1.3.1/com/soundcloud/api/package-summary.html [soundcloudapi-java]: http://code.google.com/p/soundcloudapi-java/ [soundcloudapi-java-annouce]: http://blog.soundcloud.com/2010/01/08/java-wrapper/ [CreateWrapper]: https://github.com/soundcloud/java-api-wrapper/blob/master/src/examples/java/com/soundcloud/api/examples/CreateWrapper.java diff --git a/src/main/java/com/soundcloud/api/CloudAPI.java b/src/main/java/com/soundcloud/api/CloudAPI.java index 5188e36..af551ab 100644 --- a/src/main/java/com/soundcloud/api/CloudAPI.java +++ b/src/main/java/com/soundcloud/api/CloudAPI.java @@ -39,7 +39,7 @@ public interface CloudAPI { // other constants String REALM = "SoundCloud"; String OAUTH_SCHEME = "oauth"; - String VERSION = "1.3.0"; + String VERSION = "1.3.1"; String USER_AGENT = "SoundCloud Java Wrapper ("+VERSION+")"; /** diff --git a/src/main/java/com/soundcloud/api/package-info.java b/src/main/java/com/soundcloud/api/package-info.java index 4ed2a31..51ef3a7 100644 --- a/src/main/java/com/soundcloud/api/package-info.java +++ b/src/main/java/com/soundcloud/api/package-info.java @@ -7,6 +7,6 @@ * * @see com.soundcloud.api.ApiWrapper * @author Jan Berkel - * @version 1.3.0, 22/04/13 + * @version 1.3.1, 02/10/13 */ package com.soundcloud.api; From f0d49dfc0f6299077323e39a42ed1dee940bf3ca Mon Sep 17 00:00:00 2001 From: Jan Berkel Date: Wed, 2 Oct 2013 19:24:45 +0200 Subject: [PATCH 149/156] [maven-release-plugin] prepare release 1.3.1 --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 3cad005..d302d1b 100644 --- a/pom.xml +++ b/pom.xml @@ -3,7 +3,7 @@ 4.0.0 com.soundcloud java-api-wrapper - 1.3.1-SNAPSHOT + 1.3.1 SoundCloud Java API wrapper SoundCloud Java API wrapper (OAuth2 only), works on Android https://github.com/soundcloud/java-api-wrapper#readme From 2351a8a1a454f84bbb30ee66551abcad968d50ed Mon Sep 17 00:00:00 2001 From: Jan Berkel Date: Wed, 2 Oct 2013 19:24:49 +0200 Subject: [PATCH 150/156] [maven-release-plugin] prepare for next development iteration --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index d302d1b..9e75c56 100644 --- a/pom.xml +++ b/pom.xml @@ -3,7 +3,7 @@ 4.0.0 com.soundcloud java-api-wrapper - 1.3.1 + 1.3.2-SNAPSHOT SoundCloud Java API wrapper SoundCloud Java API wrapper (OAuth2 only), works on Android https://github.com/soundcloud/java-api-wrapper#readme From 78328c98b8e79b6195e0ae5f4d699c898ff03571 Mon Sep 17 00:00:00 2001 From: Matthias Kaeppler Date: Mon, 14 Oct 2013 14:10:00 +0200 Subject: [PATCH 151/156] enable email notifications --- .travis.yml | 2 -- 1 file changed, 2 deletions(-) diff --git a/.travis.yml b/.travis.yml index c93d2de..b643cca 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,5 +1,3 @@ language: java install: gradle assemble script: gradle test -notifications: - email: false From 8418681203fa4c0d3bb4ce40b5f8b0f1c9ea4e12 Mon Sep 17 00:00:00 2001 From: Matthias Kaeppler Date: Mon, 14 Oct 2013 14:48:11 +0200 Subject: [PATCH 152/156] Add retry @Rule to retry flaky integration tests --- build.gradle | 2 +- pom.xml | 2 +- .../api/CloudAPIIntegrationTest.java | 47 ++++++++++++++++++- 3 files changed, 47 insertions(+), 4 deletions(-) diff --git a/build.gradle b/build.gradle index e1d2451..bca5905 100644 --- a/build.gradle +++ b/build.gradle @@ -20,7 +20,7 @@ dependencies { compile group: 'org.apache.httpcomponents', name: 'httpmime', version: '4.0.3' compile group: 'org.json', name: 'json', version: '20090211' - testCompile group: 'junit', name: 'junit-dep', version: '4.8.2' + testCompile group: 'junit', name: 'junit-dep', version: '4.11' testCompile group: 'org.hamcrest', name: 'hamcrest-core', version: '1.3.RC2' testCompile group: 'org.hamcrest', name: 'hamcrest-library', version: '1.3.RC2' testCompile group: 'org.mockito', name: 'mockito-core', version: '1.8.5' diff --git a/pom.xml b/pom.xml index 9e75c56..9bce0d0 100644 --- a/pom.xml +++ b/pom.xml @@ -49,7 +49,7 @@ junit junit-dep - 4.8.2 + 4.11 test diff --git a/src/test/java/com/soundcloud/api/CloudAPIIntegrationTest.java b/src/test/java/com/soundcloud/api/CloudAPIIntegrationTest.java index a75f9ac..8b2488e 100644 --- a/src/test/java/com/soundcloud/api/CloudAPIIntegrationTest.java +++ b/src/test/java/com/soundcloud/api/CloudAPIIntegrationTest.java @@ -22,7 +22,11 @@ import org.json.JSONTokener; import org.junit.Before; import org.junit.Ignore; +import org.junit.Rule; import org.junit.Test; +import org.junit.rules.TestRule; +import org.junit.runner.Description; +import org.junit.runners.model.Statement; import java.io.File; import java.io.FileInputStream; @@ -61,6 +65,9 @@ public class CloudAPIIntegrationTest implements Params.Track, Endpoints { -Dorg.apache.commons.logging.simplelog.log.org.apache.http.wire=ERROR */ + @Rule + public Retry retryRule = new Retry(3); + @Before public void setUp() throws Exception { api = new ApiWrapper( @@ -85,8 +92,8 @@ public void shouldBeAbleToMakePublicRequests() throws Exception { public void shouldUploadASimpleAudioFile() throws Exception { login(); HttpResponse resp = api.post(Request.to(TRACKS).with( - TITLE, "Hello Android", - POST_TO_EMPTY, "") + TITLE, "Hello Android", + POST_TO_EMPTY, "") .withFile(ASSET_DATA, new File(getClass().getResource("hello.aiff").getFile()))); int status = resp.getStatusLine().getStatusCode(); @@ -478,4 +485,40 @@ private JSONObject createJSONPlaylist(String title, long... trackIds) throws JSO } return json; } + + // We had trouble with some tests randomly failing, perhaps due to replication lag + // see http://stackoverflow.com/questions/8295100/how-to-re-run-failed-junit-tests-immediately + public static class Retry implements TestRule { + private int retryCount; + + public Retry(int retryCount) { + this.retryCount = retryCount; + } + + public Statement apply(Statement base, Description description) { + return statement(base, description); + } + + private Statement statement(final Statement base, final Description description) { + return new Statement() { + @Override + public void evaluate() throws Throwable { + Throwable caughtThrowable = null; + + // implement retry logic here + for (int i = 0; i < retryCount; i++) { + try { + base.evaluate(); + return; + } catch (Throwable t) { + caughtThrowable = t; + System.err.println(description.getDisplayName() + ": run " + (i+1) + " failed"); + } + } + System.err.println(description.getDisplayName() + ": giving up after " + retryCount + " failures"); + throw caughtThrowable; + } + }; + } + } } From 438aee0a75dd20231a52b7e77c3d1257e2c04c8f Mon Sep 17 00:00:00 2001 From: Jan Berkel Date: Mon, 14 Oct 2013 16:32:04 +0200 Subject: [PATCH 153/156] Update README.md --- README.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/README.md b/README.md index bed77aa..53714b6 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# java-api-wrapper +# java-api-wrapper [![Build Status](https://secure.travis-ci.org/soundcloud/java-api-wrapper.png?branch=master)](http://travis-ci.org/soundcloud/java-api-wrapper) OAuth2 SoundCloud API wrapper written in Java ([javadoc][]). @@ -233,7 +233,6 @@ Includes portions of code (c) 2010 Xtreme Labs and Pivotal Labs and (c) 2009 urb See LICENSE for details. -[![Build Status](https://secure.travis-ci.org/soundcloud/java-api-wrapper.png?branch=master)](http://travis-ci.org/soundcloud/java-api-wrapper) [gradle]: http://www.gradle.org/ [urbanstew]: http://urbanstew.org/ From 47c057b39b731c10a5770be551e116c6091dc0f0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lu=C3=ADs=20Loureiro?= Date: Sun, 10 Nov 2013 15:40:29 +0000 Subject: [PATCH 154/156] Appending display and state values to the OAuth2 authorization URI. --- src/main/java/com/soundcloud/api/ApiWrapper.java | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/main/java/com/soundcloud/api/ApiWrapper.java b/src/main/java/com/soundcloud/api/ApiWrapper.java index 8ed000f..f3c36b9 100644 --- a/src/main/java/com/soundcloud/api/ApiWrapper.java +++ b/src/main/java/com/soundcloud/api/ApiWrapper.java @@ -226,7 +226,9 @@ public Token extensionGrantType(String grantType, String... scopes) throws IOExc REDIRECT_URI, mRedirectUri, CLIENT_ID, mClientId, RESPONSE_TYPE, CODE); - if (options.length == 2) req.add(SCOPE, options[1]); + if (options.length > 1) req.add(SCOPE, options[1]); + if (options.length > 2) req.add(DISPLAY, options[2]); + if (options.length > 3) req.add(STATE, options[3]); return getURI(req, false, true); } From f38ac236178a6a3b12717eb7e197e25cdbf0cfaf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lu=C3=ADs=20Loureiro?= Date: Sun, 10 Nov 2013 15:46:51 +0000 Subject: [PATCH 155/156] Constants for the display and state OAuth2 parameters and for the popup display value. Comments related to the new options values. --- src/main/java/com/soundcloud/api/CloudAPI.java | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/main/java/com/soundcloud/api/CloudAPI.java b/src/main/java/com/soundcloud/api/CloudAPI.java index af551ab..dd42e3e 100644 --- a/src/main/java/com/soundcloud/api/CloudAPI.java +++ b/src/main/java/com/soundcloud/api/CloudAPI.java @@ -24,6 +24,8 @@ public interface CloudAPI { String CODE = "code"; String RESPONSE_TYPE = "response_type"; String SCOPE = "scope"; + String DISPLAY = "display"; + String STATE = "state"; // standard oauth2 grant types String PASSWORD = "password"; @@ -41,6 +43,7 @@ public interface CloudAPI { String OAUTH_SCHEME = "oauth"; String VERSION = "1.3.1"; String USER_AGENT = "SoundCloud Java Wrapper ("+VERSION+")"; + String POPUP = "popup"; /** * Request a token using @@ -211,7 +214,7 @@ public interface CloudAPI { *
  • error in case of failure, this contains an error code (most likely * access_denied). * - * @param options auth endpoint to use (leave out for default), requested scope (leave out for default) + * @param options auth endpoint to use (leave out for default), requested scope (leave out for default), display ('popup' for mobile optimized screen) and state. * @return the URI to open in a browser/WebView etc. * @see CloudAPI#authorizationCode(String, String...) */ From ec4228b741d59ef1128f3094d30ff4cda68e9914 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lu=C3=ADs=20Loureiro?= Date: Sun, 10 Nov 2013 15:49:19 +0000 Subject: [PATCH 156/156] Some unit tests related to the new authorization URI options values. --- .../com/soundcloud/api/ApiWrapperTest.java | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/src/test/java/com/soundcloud/api/ApiWrapperTest.java b/src/test/java/com/soundcloud/api/ApiWrapperTest.java index 4c050b1..84b2fcb 100644 --- a/src/test/java/com/soundcloud/api/ApiWrapperTest.java +++ b/src/test/java/com/soundcloud/api/ApiWrapperTest.java @@ -361,6 +361,26 @@ public void shouldIncludeScopeInAuthorizationUrl() throws Exception { ); } + @Test + public void shouldIncludeDisplayInAuthorizationUrl() throws Exception { + assertThat( + api.authorizationCodeUrl(Endpoints.CONNECT, Token.SCOPE_NON_EXPIRING, CloudAPI.POPUP).toString(), + equalTo("https://soundcloud.com/connect"+ + "?redirect_uri=redirect%3A%2F%2Fme&client_id=" + TEST_CLIENT_ID + "&response_type=code&scope=non-expiring"+ + "&display=popup") + ); + } + + @Test + public void shouldIncludeStateInAuthorizationUrl() throws Exception { + assertThat( + api.authorizationCodeUrl(Endpoints.CONNECT, Token.SCOPE_DEFAULT, CloudAPI.POPUP, "stateValue").toString(), + equalTo("https://soundcloud.com/connect"+ + "?redirect_uri=redirect%3A%2F%2Fme&client_id=" + TEST_CLIENT_ID + "&response_type=code&scope=*"+ + "&display=popup&state=stateValue") + ); + } + @Test public void shouldCallTokenStateListenerWhenTokenIsInvalidated() throws Exception { CloudAPI.TokenListener listener = mock(CloudAPI.TokenListener.class);