From 43080d2f3c8a36919a45138e14eb65bc5be35137 Mon Sep 17 00:00:00 2001 From: Jan Berkel Date: Sat, 20 Aug 2011 15:01:53 +0200 Subject: [PATCH 01/50] 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 02/50] 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 03/50] 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 04/50] 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 05/50] 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 06/50] 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 07/50] 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 08/50] 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 09/50] 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 10/50] 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 11/50] 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 12/50] 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 13/50] 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 14/50] 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 15/50] 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 16/50] 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 17/50] 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 18/50] 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 19/50] 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 20/50] 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 21/50] 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 22/50] 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 23/50] 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 24/50] 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 25/50] 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 26/50] 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 27/50] 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 28/50] 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 29/50] 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 30/50] 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 31/50] 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 32/50] 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 33/50] 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 34/50] 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 35/50] [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 36/50] [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 37/50] 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 38/50] 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 39/50] 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 40/50] 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 41/50] 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 42/50] 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 43/50] 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 44/50] 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 45/50] 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 46/50] 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 47/50] 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 48/50] 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 49/50] 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 50/50] 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);