Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 1 addition & 6 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@

<groupId>engineer.nightowl</groupId>
<artifactId>sonos-api-java</artifactId>
<version>0.0.27-SNAPSHOT</version>
<version>0.2.0-SNAPSHOT</version>
<packaging>jar</packaging>

<name>sonos-api-java</name>
Expand Down Expand Up @@ -70,11 +70,6 @@
<artifactId>commons-lang3</artifactId>
<version>3.20.0</version>
</dependency>
<dependency>
<groupId>org.apache.httpcomponents</groupId>
<artifactId>httpclient</artifactId>
<version>4.5.14</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
Expand Down
37 changes: 14 additions & 23 deletions src/main/java/engineer/nightowl/sonos/api/SonosApiClient.java
Original file line number Diff line number Diff line change
Expand Up @@ -15,13 +15,11 @@
import engineer.nightowl.sonos.api.resource.PlaylistResource;
import engineer.nightowl.sonos.api.resource.SettingsResource;

import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClientBuilder;
import org.apache.http.util.VersionInfo;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.IOException;
import java.net.http.HttpClient;
import java.util.Properties;

public class SonosApiClient implements AutoCloseable
Expand All @@ -48,7 +46,7 @@ public class SonosApiClient implements AutoCloseable

// Can be overridden by implementing applications
private SonosApiConfiguration configuration;
private CloseableHttpClient httpClient;
private HttpClient httpClient;

private final String version;

Expand All @@ -58,7 +56,7 @@ public class SonosApiClient implements AutoCloseable
* @param configuration a {@link engineer.nightowl.sonos.api.SonosApiConfiguration} containing integration
* information such as API keys
*
* @see SonosApiClient#SonosApiClient(SonosApiConfiguration, CloseableHttpClient)
* @see SonosApiClient#SonosApiClient(SonosApiConfiguration, HttpClient)
*/
public SonosApiClient(final SonosApiConfiguration configuration)
{
Expand All @@ -70,9 +68,9 @@ public SonosApiClient(final SonosApiConfiguration configuration)
*
* @param configuration a {@link engineer.nightowl.sonos.api.SonosApiConfiguration} containing integration
* information such as API keys
* @param httpClient a custom {@link CloseableHttpClient} - if null, a default client is initialised
* @param httpClient a custom {@link HttpClient} - if null, a default client is initialised
*/
public SonosApiClient(final SonosApiConfiguration configuration, final CloseableHttpClient httpClient)
public SonosApiClient(final SonosApiConfiguration configuration, final HttpClient httpClient)
{
loadProperties();
version = properties.getProperty("sonosapijava.version");
Expand Down Expand Up @@ -101,44 +99,37 @@ public SonosApiClient(final SonosApiConfiguration configuration, final Closeable

public String getUserAgent()
{
final String ahcUa = VersionInfo.getUserAgent("Apache-HttpClient",
"org.apache.http.client", HttpClientBuilder.class);

return String.format("sonos-api-java/%s (applicationId/%s) (httpClient/(%s))",
version, configuration.getApplicationId(), ahcUa);
return String.format("sonos-api-java/%s (applicationId/%s) (Java/%s)",
version, configuration.getApplicationId(), System.getProperty("java.version"));
}

/**
* Generate a default HTTP client.
*
* @return a default HTTP client.
*/
private CloseableHttpClient generateHttpClient()
private HttpClient generateHttpClient()
{
logger.debug("Using default HttpClient");
return HttpClientBuilder.create().setUserAgent(getUserAgent()).build();
return HttpClient.newBuilder()
.followRedirects(HttpClient.Redirect.NORMAL)
.build();
}

/**
* Close the HTTP client.
*/
public void closeHttpClient()
{
try
{
httpClient.close();
} catch (final IOException ioe)
{
logger.warn("Unable to close HttpClient", ioe);
}
httpClient.close();
}

/**
* Get the configured HTTP client for this API client instance.
*
* @return the configured HTTP client
*/
public CloseableHttpClient getHttpClient()
public HttpClient getHttpClient()
{
return httpClient;
}
Expand All @@ -148,7 +139,7 @@ public CloseableHttpClient getHttpClient()
*
* @param httpClient custom client to set
*/
public void setHttpClient(final CloseableHttpClient httpClient)
public void setHttpClient(final HttpClient httpClient)
{
this.httpClient = httpClient;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,8 @@

import org.apache.commons.lang3.builder.EqualsBuilder;
import org.apache.commons.lang3.builder.HashCodeBuilder;
import org.apache.http.Header;
import org.apache.http.message.BasicHeader;

import java.nio.charset.StandardCharsets;
import java.util.Base64;

/**
Expand Down Expand Up @@ -136,14 +135,13 @@ public void loadDefaults()
/**
* <p>getAuthorizationHeader.</p>
*
* @return a {@link org.apache.http.Header} object.
* @return the value to use for the {@code Authorization} header - e.g. {@code "Basic <base64>"}.
*/
public Header getAuthorizationHeader()
public String getAuthorizationHeader()
{
final byte[] authBytes = String.join(":", getApiKey(), getApiSecret()).getBytes();
final byte[] authBytes = String.join(":", getApiKey(), getApiSecret()).getBytes(StandardCharsets.UTF_8);
final String authBase64 = Base64.getEncoder().encodeToString(authBytes);
final String headerValue = String.join(" ", "Basic", authBase64);
return new BasicHeader("Authorization", headerValue);
return String.join(" ", "Basic", authBase64);
}

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,18 +5,17 @@
import engineer.nightowl.sonos.api.domain.SonosToken;
import engineer.nightowl.sonos.api.exception.SonosApiClientException;
import engineer.nightowl.sonos.api.exception.SonosApiError;
import org.apache.http.client.entity.UrlEncodedFormEntity;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.client.utils.URIBuilder;
import org.apache.http.message.BasicNameValuePair;

import java.io.UnsupportedEncodingException;
import java.math.BigInteger;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URLEncoder;
import java.net.http.HttpRequest;
import java.nio.charset.StandardCharsets;
import java.security.SecureRandom;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;

/**
* The Authorization flow is dependent on sending your user back to a pre-registered, and user-accessible
Expand Down Expand Up @@ -52,25 +51,22 @@ public AuthorizeResource(final SonosApiClient apiClient)
public URI getAuthorizeCodeUri(final String redirectUri, final String state) throws SonosApiClientException
{
final SonosApiConfiguration configuration = apiClient.getConfiguration();
final URIBuilder uri = new URIBuilder();
uri.setScheme(HTTPS);
uri.setHost(configuration.getAuthBaseUrl());
uri.setPath("/login/v3/oauth");
uri.setParameter("client_id", configuration.getApiKey());
uri.setParameter("redirect_uri", redirectUri);
final List<Map.Entry<String, String>> params = new ArrayList<>();
params.add(Map.entry("client_id", configuration.getApiKey()));
params.add(Map.entry("redirect_uri", redirectUri));
// Only currently supported values by Sonos
uri.setParameter("response_type", "code");
uri.setParameter("scope", "playback-control-all");
params.add(Map.entry("response_type", "code"));
params.add(Map.entry("scope", "playback-control-all"));
// State is optional, only include it if set.
if (state != null)
{
uri.setParameter("state", state);
params.add(Map.entry("state", state));
}

try
{
return uri.build();
} catch (final URISyntaxException e)
return URI.create(String.format("%s://%s/login/v3/oauth?%s", HTTPS, configuration.getAuthBaseUrl(), encodeParameters(params)));
} catch (final IllegalArgumentException e)
{
throw new SonosApiClientException("Invalid URI built", e);
}
Expand Down Expand Up @@ -102,10 +98,10 @@ public URI getAuthorizeCodeUri(final String redirectUri) throws SonosApiClientEx
public SonosToken createToken(final String redirectUri, final String authorizeCode) throws SonosApiClientException,
SonosApiError
{
final List<BasicNameValuePair> postParameters = new ArrayList<>();
postParameters.add(new BasicNameValuePair("redirect_uri", redirectUri));
postParameters.add(new BasicNameValuePair("code", authorizeCode));
postParameters.add(new BasicNameValuePair("grant_type", "authorization_code"));
final List<Map.Entry<String, String>> postParameters = new ArrayList<>();
postParameters.add(Map.entry("redirect_uri", redirectUri));
postParameters.add(Map.entry("code", authorizeCode));
postParameters.add(Map.entry("grant_type", "authorization_code"));

return callApi(buildOauthAccessRequest(postParameters), SonosToken.class);
}
Expand All @@ -121,9 +117,9 @@ public SonosToken createToken(final String redirectUri, final String authorizeCo
*/
public SonosToken refreshToken(final String refreshToken) throws SonosApiClientException, SonosApiError
{
final List<BasicNameValuePair> postParameters = new ArrayList<>();
postParameters.add(new BasicNameValuePair("refresh_token", refreshToken));
postParameters.add(new BasicNameValuePair("grant_type", "refresh_token"));
final List<Map.Entry<String, String>> postParameters = new ArrayList<>();
postParameters.add(Map.entry("refresh_token", refreshToken));
postParameters.add(Map.entry("grant_type", "refresh_token"));

return callApi(buildOauthAccessRequest(postParameters), SonosToken.class);
}
Expand All @@ -136,33 +132,42 @@ public SonosToken refreshToken(final String refreshToken) throws SonosApiClientE
* @return the built request, ready to execute via {@link #callApi}
* @throws SonosApiClientException if the request could not be built
*/
private HttpPost buildOauthAccessRequest(final List<BasicNameValuePair> postParameters) throws SonosApiClientException
private HttpRequest buildOauthAccessRequest(final List<Map.Entry<String, String>> postParameters) throws SonosApiClientException
{
final SonosApiConfiguration configuration = apiClient.getConfiguration();
final URIBuilder uri = new URIBuilder();
uri.setScheme(HTTPS);
uri.setHost(configuration.getAuthBaseUrl());
uri.setPath("/login/v3/oauth/access");

final HttpPost request = new HttpPost();
try
{
request.setEntity(new UrlEncodedFormEntity(postParameters));
} catch (final UnsupportedEncodingException e)
{
throw new SonosApiClientException("Unable to generate auth request content", e);
}
request.setHeader(configuration.getAuthorizationHeader());

final URI uri;
try
{
request.setURI(uri.build());
} catch (final URISyntaxException e)
uri = URI.create(String.format("%s://%s/login/v3/oauth/access", HTTPS, configuration.getAuthBaseUrl()));
} catch (final IllegalArgumentException e)
{
throw new SonosApiClientException("Invalid URI built", e);
}

return request;
return HttpRequest.newBuilder(uri)
.setHeader("Authorization", configuration.getAuthorizationHeader())
.setHeader("Content-Type", "application/x-www-form-urlencoded; charset=UTF-8")
.setHeader("User-Agent", apiClient.getUserAgent())
.POST(HttpRequest.BodyPublishers.ofString(encodeParameters(postParameters), StandardCharsets.UTF_8))
.build();
}

/**
* URL-encode and join a list of form/query parameters as {@code key=value&key2=value2}.
*
* @param parameters the parameters to encode, in order
* @return the encoded, joined parameter string
*/
private static String encodeParameters(final List<Map.Entry<String, String>> parameters)
{
return parameters.stream()
.map(entry -> encode(entry.getKey()) + "=" + encode(entry.getValue()))
.collect(Collectors.joining("&"));
}

private static String encode(final String value)
{
return URLEncoder.encode(value, StandardCharsets.UTF_8);
}

/**
Expand Down
Loading
Loading