com.fasterxml.jackson.core
jackson-databind
diff --git a/src/main/java/engineer/nightowl/sonos/api/SonosApiClient.java b/src/main/java/engineer/nightowl/sonos/api/SonosApiClient.java
index 3bc4c98..18c9608 100644
--- a/src/main/java/engineer/nightowl/sonos/api/SonosApiClient.java
+++ b/src/main/java/engineer/nightowl/sonos/api/SonosApiClient.java
@@ -15,16 +15,14 @@
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
+public class SonosApiClient
{
// Resources
private final AudioClipResource audioClipResource;
@@ -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;
@@ -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)
{
@@ -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");
@@ -101,11 +99,8 @@ 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))",
+ version, configuration.getApplicationId());
}
/**
@@ -113,24 +108,10 @@ public String getUserAgent()
*
* @return a default HTTP client.
*/
- private CloseableHttpClient generateHttpClient()
+ private HttpClient generateHttpClient()
{
logger.debug("Using default HttpClient");
- return HttpClientBuilder.create().setUserAgent(getUserAgent()).build();
- }
-
- /**
- * Close the HTTP client.
- */
- public void closeHttpClient()
- {
- try
- {
- httpClient.close();
- } catch (final IOException ioe)
- {
- logger.warn("Unable to close HttpClient", ioe);
- }
+ return HttpClient.newHttpClient();
}
/**
@@ -138,7 +119,7 @@ public void closeHttpClient()
*
* @return the configured HTTP client
*/
- public CloseableHttpClient getHttpClient()
+ public HttpClient getHttpClient()
{
return httpClient;
}
@@ -148,7 +129,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;
}
@@ -323,13 +304,4 @@ public SettingsResource settings()
return settingsResource;
}
-
- /**
- * Closes the HttpClient
- */
- @Override
- public void close()
- {
- closeHttpClient();
- }
}
diff --git a/src/main/java/engineer/nightowl/sonos/api/SonosApiConfiguration.java b/src/main/java/engineer/nightowl/sonos/api/SonosApiConfiguration.java
index f3f4e20..d8fabbf 100644
--- a/src/main/java/engineer/nightowl/sonos/api/SonosApiConfiguration.java
+++ b/src/main/java/engineer/nightowl/sonos/api/SonosApiConfiguration.java
@@ -1,13 +1,11 @@
package engineer.nightowl.sonos.api;
-import org.apache.commons.codec.binary.Base64;
-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.util.Base64;
+import java.util.Objects;
/**
- * Configuration class to be built up and passed into a {@link engineer.nightowl.sonos.api.SonosApiClient}
+ * Configuration class to be built up and passed into a
+ * {@link engineer.nightowl.sonos.api.SonosApiClient}
*
* Loads defaults on construction.
*/
@@ -21,172 +19,92 @@ public class SonosApiConfiguration
private String controlBaseUrl;
private Boolean clientSideValidationEnabled;
- /**
- *
Constructor for SonosApiConfiguration.
- */
- public SonosApiConfiguration()
- {
+ public SonosApiConfiguration() {
loadDefaults();
}
- /**
- * Getter for the field applicationId.
- *
- * @return a {@link java.lang.String} object.
- */
- public String getApplicationId()
- {
+ public String getApplicationId() {
return applicationId;
}
- /**
- * Setter for the field applicationId.
- *
- * @param applicationId a {@link java.lang.String} object.
- */
- public void setApplicationId(final String applicationId)
- {
+ public void setApplicationId(final String applicationId) {
this.applicationId = applicationId;
}
- /**
- * Getter for the field apiKey.
- *
- * @return a {@link java.lang.String} object.
- */
- public String getApiKey()
- {
+ public String getApiKey() {
return apiKey;
}
- /**
- * Setter for the field apiKey.
- *
- * @param apiKey a {@link java.lang.String} object.
- */
- public void setApiKey(final String apiKey)
- {
+ public void setApiKey(final String apiKey) {
this.apiKey = apiKey;
}
- public String getApiSecret()
- {
+ public String getApiSecret() {
return apiSecret;
}
- /**
- * Setter for the field apiSecret.
- *
- * @param apiSecret a {@link java.lang.String} object.
- */
- public void setApiSecret(final String apiSecret)
- {
+ public void setApiSecret(final String apiSecret) {
this.apiSecret = apiSecret;
}
- /**
- * Getter for the field authBaseUrl.
- *
- * @return a {@link java.lang.String} object.
- */
- public String getAuthBaseUrl()
- {
+ public String getAuthBaseUrl() {
return authBaseUrl;
}
- public void setAuthBaseUrl(final String authBaseUrl)
- {
+ public void setAuthBaseUrl(final String authBaseUrl) {
this.authBaseUrl = authBaseUrl;
}
- /**
- * Getter for the field controlBaseUrl.
- *
- * @return a {@link java.lang.String} object.
- */
- public String getControlBaseUrl()
- {
+ public String getControlBaseUrl() {
return controlBaseUrl;
}
- public void setControlBaseUrl(final String controlBaseUrl)
- {
+ public void setControlBaseUrl(final String controlBaseUrl) {
this.controlBaseUrl = controlBaseUrl;
}
- public Boolean isClientSideValidationEnabled()
- {
+ public Boolean isClientSideValidationEnabled() {
return clientSideValidationEnabled;
}
- public void setClientSideValidationEnabled(Boolean clientSideValidationEnabled)
- {
+ public void setClientSideValidationEnabled(Boolean clientSideValidationEnabled) {
this.clientSideValidationEnabled = clientSideValidationEnabled;
}
-
- public void loadDefaults()
- {
+ public void loadDefaults() {
setAuthBaseUrl("api.sonos.com");
setControlBaseUrl("api.ws.sonos.com/control/api");
setClientSideValidationEnabled(Boolean.TRUE);
}
- /**
- * getAuthorizationHeader.
- *
- * @return a {@link org.apache.http.Header} object.
- */
- public Header getAuthorizationHeader()
- {
+ public String getAuthorizationHeaderValue() {
final byte[] authBytes = String.join(":", getApiKey(), getApiSecret()).getBytes();
- final String authBase64 = Base64.encodeBase64String(authBytes);
- final String headerValue = String.join(" ", "Basic", authBase64);
- return new BasicHeader("Authorization", headerValue);
+ final String authBase64 = Base64.getEncoder().encodeToString(authBytes);
+ return String.join(" ", "Basic", authBase64);
}
@Override
- public String toString()
- {
- return "SonosApiConfiguration{" +
- "applicationId='" + applicationId + '\'' +
- ", apiKey='" + apiKey + '\'' +
- ", apiSecret='" + apiSecret + '\'' +
- ", authBaseUrl='" + authBaseUrl + '\'' +
- ", controlBaseUrl='" + controlBaseUrl + '\'' +
- ", clientSideValidationEnabled=" + clientSideValidationEnabled +
- '}';
+ public String toString() {
+ return "SonosApiConfiguration [apiKey=" + apiKey + ", apiSecret=" + apiSecret + ", applicationId="
+ + applicationId + ", authBaseUrl=" + authBaseUrl + ", clientSideValidationEnabled="
+ + clientSideValidationEnabled + ", controlBaseUrl=" + controlBaseUrl + "]";
}
@Override
- public boolean equals(Object o)
- {
- if (this == o) return true;
-
- if (o == null || getClass() != o.getClass()) return false;
-
- SonosApiConfiguration that = (SonosApiConfiguration) o;
-
- return new EqualsBuilder()
- .append(applicationId, that.applicationId)
- .append(apiKey, that.apiKey)
- .append(apiSecret, that.apiSecret)
- .append(authBaseUrl, that.authBaseUrl)
- .append(controlBaseUrl, that.controlBaseUrl)
- .append(clientSideValidationEnabled, that.clientSideValidationEnabled)
- .isEquals();
+ public boolean equals(Object obj) {
+ if (this == obj)
+ return true;
+ if (!(obj instanceof SonosApiConfiguration))
+ return false;
+ SonosApiConfiguration other = (SonosApiConfiguration) obj;
+ return Objects.equals(apiKey, other.apiKey) && Objects.equals(apiSecret, other.apiSecret)
+ && Objects.equals(applicationId, other.applicationId) && Objects.equals(authBaseUrl, other.authBaseUrl)
+ && Objects.equals(clientSideValidationEnabled, other.clientSideValidationEnabled)
+ && Objects.equals(controlBaseUrl, other.controlBaseUrl);
}
@Override
- public int hashCode()
- {
- return new HashCodeBuilder(17, 37)
- .append(applicationId)
- .append(apiKey)
- .append(apiSecret)
- .append(authBaseUrl)
- .append(controlBaseUrl)
- .append(clientSideValidationEnabled)
- .toHashCode();
+ public int hashCode() {
+ return Objects.hash(apiKey, apiSecret, applicationId, authBaseUrl, clientSideValidationEnabled, controlBaseUrl);
}
}
diff --git a/src/main/java/engineer/nightowl/sonos/api/domain/SonosSessionError.java b/src/main/java/engineer/nightowl/sonos/api/domain/SonosSessionError.java
index ff5e266..1fa0ece 100644
--- a/src/main/java/engineer/nightowl/sonos/api/domain/SonosSessionError.java
+++ b/src/main/java/engineer/nightowl/sonos/api/domain/SonosSessionError.java
@@ -8,6 +8,7 @@
*/
public class SonosSessionError extends SonosApiError
{
+ private static final long serialVersionUID = 9183301150166556145L;
private String sessionId;
private SonosSessionErrorCode errorCode;
}
diff --git a/src/main/java/engineer/nightowl/sonos/api/resource/AuthorizeResource.java b/src/main/java/engineer/nightowl/sonos/api/resource/AuthorizeResource.java
index dfe04e6..9c6c69d 100644
--- a/src/main/java/engineer/nightowl/sonos/api/resource/AuthorizeResource.java
+++ b/src/main/java/engineer/nightowl/sonos/api/resource/AuthorizeResource.java
@@ -5,18 +5,16 @@
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.http.HttpRequest;
+import java.net.http.HttpRequest.BodyPublishers;
import java.security.SecureRandom;
-import java.util.ArrayList;
-import java.util.List;
+import java.util.HashMap;
+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
@@ -52,22 +50,21 @@ public AuthorizeResource(final SonosApiClient apiClient)
public URI getAuthorizeCodeUri(final String redirectUri, final String state) throws URISyntaxException
{
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 Map params = new HashMap<>(5);
+ params.put("client_id", configuration.getApiKey());
+ params.put("redirect_uri", redirectUri);
// Only currently supported values by Sonos
- uri.setParameter("response_type", "code");
- uri.setParameter("scope", "playback-control-all");
+ params.put("response_type", "code");
+ params.put("scope", "playback-control-all");
// State is optional, only include it if set.
if (state != null)
{
- uri.setParameter("state", state);
+ params.put("state", state);
}
- return uri.build();
+ final String query = params.keySet().stream().map(key -> (key + "=" + params.get(key))).collect(Collectors.joining("&"));
+
+ return new URI(HTTPS, configuration.getAuthBaseUrl(), "/login/v3/oauth", query, null);
}
/**
@@ -97,39 +94,25 @@ public SonosToken createToken(final String redirectUri, final String authorizeCo
SonosApiError
{
final SonosApiConfiguration configuration = apiClient.getConfiguration();
- final URIBuilder uri = new URIBuilder();
- uri.setScheme(HTTPS);
- uri.setHost(configuration.getAuthBaseUrl());
- uri.setPath("/login/v3/oauth/access");
-
- final BasicNameValuePair redirectUriParameter = new BasicNameValuePair("redirect_uri", redirectUri);
- final BasicNameValuePair code = new BasicNameValuePair("code", authorizeCode);
- final BasicNameValuePair grantType = new BasicNameValuePair("grant_type", "authorization_code");
- final HttpPost request = new HttpPost();
-
- final List postParameters = new ArrayList<>();
- postParameters.add(redirectUriParameter);
- postParameters.add(code);
- postParameters.add(grantType);
-
- try
- {
- request.setEntity(new UrlEncodedFormEntity(postParameters));
- } catch (final UnsupportedEncodingException e)
- {
- throw new SonosApiClientException("Unable to generate auth request content", e);
+ final HttpRequest.Builder request = HttpRequest.newBuilder();
+ URI uri;
+ try {
+ uri = new URI(HTTPS, configuration.getAuthBaseUrl(), "/login/v3/oauth/access", null, null);
+ } catch (URISyntaxException e) {
+ throw new SonosApiClientException("Invalid URI constructed", e);
}
- request.setHeader(configuration.getAuthorizationHeader());
- try
- {
- request.setURI(uri.build());
- } catch (final URISyntaxException e)
- {
- throw new SonosApiClientException("Invalid URI built", e);
- }
+ request.setHeader("Authorization", configuration.getAuthorizationHeaderValue());
+ request.uri(uri);
+
+ final Map params = new HashMap<>(3);
+ params.put("redirect_uri", redirectUri);
+ params.put("code", authorizeCode);
+ params.put("grant_type", "authorization_code");
+
+ request.POST(BodyPublishers.ofString(params.keySet().stream().map(key -> (key + "=" + params.get(key))).collect(Collectors.joining("&"))));
- return callApi(request, SonosToken.class);
+ return callApi(request.build(), SonosToken.class);
}
/**
@@ -144,42 +127,24 @@ public SonosToken createToken(final String redirectUri, final String authorizeCo
public SonosToken refreshToken(final String refreshToken) throws SonosApiClientException, SonosApiError
{
final SonosApiConfiguration configuration = apiClient.getConfiguration();
-
- // Setup URI
- final URIBuilder uri = new URIBuilder();
- uri.setScheme(HTTPS);
- uri.setHost(configuration.getAuthBaseUrl());
- uri.setPath("/login/v3/oauth/access");
-
- // Setup POST contents
- final HttpPost request = new HttpPost();
- final BasicNameValuePair refreshTokenParameter = new BasicNameValuePair("refresh_token", refreshToken);
- final BasicNameValuePair grantType = new BasicNameValuePair("grant_type", "refresh_token");
- final List postParameters = new ArrayList<>();
- postParameters.add(refreshTokenParameter);
- postParameters.add(grantType);
-
- try
- {
- request.setEntity(new UrlEncodedFormEntity(postParameters));
- } catch (final UnsupportedEncodingException e)
- {
- throw new SonosApiClientException("Unable to generate auth request content", e);
+ final HttpRequest.Builder request = HttpRequest.newBuilder();
+ URI uri;
+ try {
+ uri = new URI(HTTPS, configuration.getAuthBaseUrl(), "/login/v3/oauth/access", null, null);
+ } catch (URISyntaxException e) {
+ throw new SonosApiClientException("Invalid URI constructed", e);
}
- // Set authorization header
- request.setHeader(configuration.getAuthorizationHeader());
+ request.setHeader("Authorization", configuration.getAuthorizationHeaderValue());
+ request.uri(uri);
- // Execute request
- try
- {
- request.setURI(uri.build());
- } catch (final URISyntaxException e)
- {
- throw new SonosApiClientException("Invalid URI built", e);
- }
+ final Map params = new HashMap<>(3);
+ params.put("refresh_token", refreshToken);
+ params.put("grant_type", "refresh_token");
+
+ request.POST(BodyPublishers.ofString(params.keySet().stream().map(key -> (key + "=" + params.get(key))).collect(Collectors.joining("&"))));
- return callApi(request, SonosToken.class);
+ return callApi(request.build(), SonosToken.class);
}
/**
diff --git a/src/main/java/engineer/nightowl/sonos/api/resource/BaseResource.java b/src/main/java/engineer/nightowl/sonos/api/resource/BaseResource.java
index 0f0cc94..80a46fb 100644
--- a/src/main/java/engineer/nightowl/sonos/api/resource/BaseResource.java
+++ b/src/main/java/engineer/nightowl/sonos/api/resource/BaseResource.java
@@ -11,88 +11,105 @@
import engineer.nightowl.sonos.api.exception.SonosApiError;
import engineer.nightowl.sonos.api.specs.Validatable;
import engineer.nightowl.sonos.api.util.SonosUtilityHelper;
-import org.apache.http.Header;
-import org.apache.http.client.methods.CloseableHttpResponse;
-import org.apache.http.client.methods.HttpDelete;
-import org.apache.http.client.methods.HttpGet;
-import org.apache.http.client.methods.HttpPost;
-import org.apache.http.client.methods.HttpRequestBase;
-import org.apache.http.client.methods.HttpUriRequest;
-import org.apache.http.client.utils.URIBuilder;
-import org.apache.http.entity.ContentType;
-import org.apache.http.entity.StringEntity;
+
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.IOException;
import java.io.InputStream;
+import java.net.URI;
import java.net.URISyntaxException;
+import java.net.http.HttpHeaders;
+import java.net.http.HttpRequest;
+import java.net.http.HttpResponse;
+import java.net.http.HttpRequest.BodyPublishers;
+import java.net.http.HttpResponse.BodyHandlers;
+import java.util.Optional;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.CompletionException;
+import java.util.concurrent.ExecutionException;
/**
* Generic base class used for integrating with the Sonos API.
*/
-class BaseResource
-{
+class BaseResource {
// Default ObjectMapper.
private static final ObjectMapper OM = new ObjectMapper()
// Allow unknown properties - these may be added by Sonos, but Jackson will
// error if not specified in the relevant POJO
.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false)
.setSerializationInclusion(JsonInclude.Include.NON_NULL);
+
+ private static final String ASYNC_ERROR_MSG = "Failure during async method execution";
/**
* Sonos can provide a header describing the response type
*
* @see engineer.nightowl.sonos.api.enums.SonosType
*/
static final String SONOS_TYPE_HEADER = "X-Sonos-Type";
- private final Logger logger = LoggerFactory.getLogger(getClass());
+ protected final Logger logger = LoggerFactory.getLogger(getClass());
SonosApiClient apiClient;
- BaseResource(final SonosApiClient apiClient)
- {
+ BaseResource(final SonosApiClient apiClient) {
this.apiClient = apiClient;
}
/**
- * Main API call method. Takes in a {@link HttpUriRequest} comprising of a
- * URI and method
+ * Main API call method. Takes in a {@link HttpUriRequest} comprising of a URI
+ * and method
*
* @param request with the relevant URI and method (with associated data as
* appropriate)
* @param type what the response should interpreted as
* @return the type specified in 'type'
* @throws SonosApiClientException if unable to build or execute the request
- * @throws SonosApiError if the Sonos API returns an error to an otherwise successful request
+ * @throws SonosApiError if the Sonos API returns an error to an
+ * otherwise successful request
+ * @throws InterruptedException
*/
- T callApi(final HttpUriRequest request, final Class type) throws SonosApiClientException, SonosApiError
+ CompletableFuture callApiAsync(final HttpRequest request, final Class type)
+ throws SonosApiClientException, SonosApiError
{
- logger.debug("Sending request to {}", request.getURI());
- final CloseableHttpResponse response;
- try
- {
- response = apiClient.getHttpClient().execute(request);
- } catch (final IOException e)
- {
- throw new SonosApiClientException("Error interrogating Sonos API", e);
+ logger.debug("Sending request to {}", request.uri());
+ return apiClient.getHttpClient().sendAsync(request, BodyHandlers.ofInputStream()).thenApply(resp -> handleApiResponseAsync(resp, type));
+ }
+
+ T callApi(final HttpRequest request, final Class type) throws SonosApiClientException, SonosApiError {
+ try {
+ return callApiAsync(request, type).get();
+ } catch (InterruptedException | ExecutionException e) {
+ Thread.currentThread().interrupt();
+ throw new SonosApiClientException(ASYNC_ERROR_MSG, e);
}
- if (401 == response.getStatusLine().getStatusCode())
+ }
+
+ T handleApiResponseAsync(final HttpResponse response, final Class type)
+ {
+ if (401 == response.statusCode())
{
- throw new SonosApiClientException("Invalid token");
+ throw new CompletionException(new SonosApiClientException("Invalid token"));
}
+
final byte[] bytes;
- try (final InputStream stream = response.getEntity().getContent())
+ try (final InputStream stream = response.body())
{
bytes = stream.readAllBytes();
} catch (final IOException ioe)
{
- throw new SonosApiClientException("Unable to convert response body", ioe);
+ throw new CompletionException(new SonosApiClientException("Unable to convert response body", ioe));
}
logger.debug("Raw response from API: {}", response);
logger.debug("Raw response content from API: {}", bytes);
// Get type from Sonos response - not always possible
- final SonosType sonosDeclaredClass = getTypeFromHeader(response);
+ SonosType sonosDeclaredClass;
+ try {
+ sonosDeclaredClass = getTypeFromHeader(response.headers());
+ } catch (SonosApiClientException sace) {
+ throw new CompletionException(sace);
+ }
+
final String sonosDeclaredClassName = sonosDeclaredClass == null ? null : sonosDeclaredClass.getClazz().getSimpleName();
// If Sonos didn't provide a type, or if one was provided and it matches what we wanted returned, proceed
@@ -104,7 +121,7 @@ T callApi(final HttpUriRequest request, final Class type) throws SonosApi
} catch (final IOException | ClassNotFoundException e)
{
final String msg = String.format("Unexpected error converting response to %s (Sonos declared %s)", type.getSimpleName(), sonosDeclaredClassName);
- throw new SonosApiClientException(msg, e);
+ throw new CompletionException(new SonosApiClientException(msg, e));
}
}
// Otherwise it's not what we expected - likely an error object, in which case throw an exception with the mapped object
@@ -116,16 +133,16 @@ T callApi(final HttpUriRequest request, final Class type) throws SonosApi
responseContent = OM.readValue(bytes, sonosDeclaredClass.getClazz());
} catch (final IOException e)
{
- throw new SonosApiClientException("Unable to parse error response from Sonos", e);
+ throw new CompletionException(new SonosApiClientException("Unable to parse error response from Sonos", e));
}
if (SonosType.getErrorTypes().contains(sonosDeclaredClass))
{
- throw (SonosApiError) sonosDeclaredClass.getClazz().cast(responseContent);
+ throw new CompletionException((SonosApiError) sonosDeclaredClass.getClazz().cast(responseContent));
} else
{
final String mismatchMsg = String.format("Sonos declared %s as the response type, but the integration requested %s", sonosDeclaredClassName, type.getSimpleName());
- throw new SonosApiClientException(mismatchMsg);
+ throw new CompletionException(new SonosApiClientException(mismatchMsg));
}
}
}
@@ -136,25 +153,22 @@ T callApi(final HttpUriRequest request, final Class type) throws SonosApi
* @param response - raw response to fetch the header from
* @return the {@link SonosType} declared, or null if not found
*/
- SonosType getTypeFromHeader(final CloseableHttpResponse response) throws SonosApiClientException
+ SonosType getTypeFromHeader(final HttpHeaders headers) throws SonosApiClientException
{
- if (response != null)
+ final Optional header = headers.firstValue(SONOS_TYPE_HEADER);
+ if (header.isPresent() && !SonosUtilityHelper.isEmpty(header.get()))
{
- final Header header = response.getFirstHeader(SONOS_TYPE_HEADER);
- if (header != null && !SonosUtilityHelper.isEmpty(header.getValue()))
+ final String headerValue = header.get();
+ if (!"none".equalsIgnoreCase(headerValue))
{
- final String headerValue = header.getValue();
- if (!"none".equalsIgnoreCase(headerValue))
+ try
{
- try
- {
- return SonosType.valueOf(headerValue);
- }
- catch (final IllegalArgumentException iae)
- {
- final String msg = String.format("Unexpected return type [%s] - please raise a bug", headerValue);
- throw new SonosApiClientException(msg, iae);
- }
+ return SonosType.valueOf(headerValue);
+ }
+ catch (final IllegalArgumentException iae)
+ {
+ final String msg = String.format("Unexpected return type [%s] - please raise a bug", headerValue);
+ throw new SonosApiClientException(msg, iae);
}
}
}
@@ -171,11 +185,13 @@ SonosType getTypeFromHeader(final CloseableHttpResponse response) throws SonosAp
* @return the response from the Sonos API as the specified type
* @throws SonosApiClientException if an error occurs during the call
* @throws SonosApiError if the API returns an error
+ * @throws ExecutionException
+ * @throws InterruptedException
*/
- T getFromApi(final Class returnType, final String token, final String path) throws SonosApiClientException, SonosApiError
+ T getFromApi(final Class returnType, final String token, final String path)
+ throws SonosApiClientException, SonosApiError
{
- final HttpGet request = getGetRequest(token, path);
- return callApi(request, returnType);
+ return callApi(buildGetRequest(token, path).build(), returnType);
}
/**
@@ -188,11 +204,13 @@ T getFromApi(final Class returnType, final String token, final String pat
* @return the response from the Sonos API as the specified type
* @throws SonosApiClientException if an error occurs during the call
* @throws SonosApiError if the API returns an error
+ * @throws ExecutionException
+ * @throws InterruptedException
*/
- T deleteFromApi(final Class returnType, final String token, final String path) throws SonosApiClientException, SonosApiError
+ T deleteFromApi(final Class returnType, final String token, final String path)
+ throws SonosApiClientException, SonosApiError
{
- final HttpDelete request = getDeleteRequest(token, path);
- return callApi(request, returnType);
+ return callApi(buildDeleteRequest(token, path).build(), returnType);
}
/**
@@ -205,10 +223,34 @@ T deleteFromApi(final Class returnType, final String token, final String
* @return the response from the Sonos API as the specified type
* @throws SonosApiClientException if an error occurs during the call
* @throws SonosApiError if the API returns an error
+ * @throws ExecutionException
+ * @throws InterruptedException
*/
- T postToApi(final Class returnType, final String token, final String path) throws SonosApiClientException, SonosApiError
+ T postToApi(final Class returnType, final String token, final String path)
+ throws SonosApiClientException, SonosApiError
+ {
+ try {
+ return postToApiAsync(returnType, token, path).get();
+ } catch (InterruptedException | ExecutionException e) {
+ Thread.currentThread().interrupt();
+ throw new SonosApiClientException(ASYNC_ERROR_MSG, e);
+ }
+ }
+
+ CompletableFuture postToApiAsync(final Class returnType, final String token, final String path) throws SonosApiClientException, SonosApiError
{
- return postToApi(returnType, token, path, new String[0]);
+ return postToApiAsync(returnType, token, path, null);
+ }
+
+ T postToApi(final Class returnType, final String token, final String path,
+ final U content) throws SonosApiClientException, SonosApiError
+ {
+ try {
+ return postToApiAsync(returnType, token, path, content).get();
+ } catch (InterruptedException | ExecutionException e) {
+ Thread.currentThread().interrupt();
+ throw new SonosApiClientException(ASYNC_ERROR_MSG, e);
+ }
}
/**
@@ -224,10 +266,10 @@ T postToApi(final Class returnType, final String token, final String path
* @throws SonosApiClientException if an error occurs during the call
* @throws SonosApiError if the API returns an error
*/
- T postToApi(final Class returnType, final String token, final String path,
+ CompletableFuture postToApiAsync(final Class returnType, final String token, final String path,
final U content) throws SonosApiClientException, SonosApiError
{
- final HttpPost request = getPostRequest(token, path);
+ final HttpRequest.Builder request = buildPostRequest(token, path);
final Boolean validationEnabled = apiClient.getConfiguration().isClientSideValidationEnabled();
// If the content for the request has the ability to be validated, do so if enabled.
// If the object is invalid, there's no point sending it to the API to be rejected.
@@ -238,18 +280,17 @@ T postToApi(final Class returnType, final String token, final String p
if (!SonosUtilityHelper.isEmpty(content))
{
final String json;
- final StringEntity requestContent;
try
{
json = OM.writeValueAsString(content);
- requestContent = new StringEntity(json, ContentType.APPLICATION_JSON);
} catch (final JsonProcessingException e)
{
throw new SonosApiClientException("Unable to convert POST request parameters", e);
}
- request.setEntity(requestContent);
+ request.POST(BodyPublishers.ofString(json));
}
- return callApi(request, returnType);
+
+ return callApiAsync(request.build(), returnType);
}
/**
@@ -262,33 +303,19 @@ T postToApi(final Class returnType, final String token, final String p
* @return a generic request
* @throws SonosApiClientException if an error occurs building the request
*/
- T getStandardRequest(final Class requestType, final String token,
+ HttpRequest.Builder buildStandardRequest(final HttpRequest.Builder request, final String token,
final String path) throws SonosApiClientException
{
- final T request;
- try
- {
- request = requestType.getDeclaredConstructor().newInstance();
- } catch (final Exception e)
- {
- throw new SonosApiClientException("Unable to create class " + requestType.getSimpleName(), e);
- }
-
final SonosApiConfiguration configuration = apiClient.getConfiguration();
- final URIBuilder uri = new URIBuilder();
- uri.setScheme("https");
- uri.setHost(configuration.getControlBaseUrl());
- uri.setPath(path);
+ URI uri;
+ try {
+ uri = new URI("https", configuration.getControlBaseUrl(), path, null);
+ } catch (URISyntaxException e) {
+ throw new SonosApiClientException("Invalid URI constructed", e);
+ }
request.setHeader("Authorization", String.format("Bearer %s", token));
-
- try
- {
- request.setURI(uri.build());
- } catch (final URISyntaxException e)
- {
- throw new SonosApiClientException("Invalid URI built", e);
- }
+ request.uri(uri);
return request;
}
@@ -301,9 +328,9 @@ T getStandardRequest(final Class requestType, fin
* @return a basic GET request
* @throws SonosApiClientException if an error occurs building the request
*/
- HttpGet getGetRequest(final String token, final String path) throws SonosApiClientException
+ HttpRequest.Builder buildGetRequest(final String token, final String path) throws SonosApiClientException
{
- return getStandardRequest(HttpGet.class, token, path);
+ return buildStandardRequest(HttpRequest.newBuilder().GET(), token, path);
}
/**
@@ -314,9 +341,9 @@ HttpGet getGetRequest(final String token, final String path) throws SonosApiClie
* @return a basic DELETE request
* @throws SonosApiClientException if an error occurs building the request
*/
- HttpDelete getDeleteRequest(final String token, final String path) throws SonosApiClientException
+ HttpRequest.Builder buildDeleteRequest(final String token, final String path) throws SonosApiClientException
{
- return getStandardRequest(HttpDelete.class, token, path);
+ return buildStandardRequest(HttpRequest.newBuilder().DELETE(), token, path);
}
/**
@@ -327,9 +354,9 @@ HttpDelete getDeleteRequest(final String token, final String path) throws SonosA
* @return a basic POST request
* @throws SonosApiClientException if an error occurs building the request
*/
- HttpPost getPostRequest(final String token, final String path) throws SonosApiClientException
+ HttpRequest.Builder buildPostRequest(final String token, final String path) throws SonosApiClientException
{
- return getStandardRequest(HttpPost.class, token, path);
+ return buildStandardRequest(HttpRequest.newBuilder().POST(BodyPublishers.ofString(null)), token, path);
}
void validateNotNull(final Object o) throws SonosApiClientException
diff --git a/src/main/java/engineer/nightowl/sonos/api/util/SignatureHeader.java b/src/main/java/engineer/nightowl/sonos/api/util/SignatureHeader.java
new file mode 100644
index 0000000..9cbf319
--- /dev/null
+++ b/src/main/java/engineer/nightowl/sonos/api/util/SignatureHeader.java
@@ -0,0 +1,35 @@
+package engineer.nightowl.sonos.api.util;
+
+import java.util.ArrayList;
+import java.util.List;
+
+public enum SignatureHeader {
+ SEQUENCE_ID("X-Sonos-Event-Seq-Id"),
+ NAMESPACE("X-Sonos-Namespace"),
+ TYPE("X-Sonos-Type"),
+ TARGET_TYPE("X-Sonos-Target-Type"),
+ TARGET_VALUE("X-Sonos-Target-Value");
+
+ private String headerKey;
+
+ SignatureHeader(final String headerKey)
+ {
+ this.headerKey = headerKey;
+ }
+
+ public String getHeaderKey()
+ {
+ return headerKey;
+ }
+
+ public static List getAllHeaders()
+ {
+ final List headers = new ArrayList<>(5);
+ headers.add(SEQUENCE_ID.headerKey);
+ headers.add(NAMESPACE.headerKey);
+ headers.add(TYPE.headerKey);
+ headers.add(TARGET_TYPE.headerKey);
+ headers.add(TARGET_VALUE.headerKey);
+ return headers;
+ }
+}
diff --git a/src/main/java/engineer/nightowl/sonos/api/util/SonosCallbackHelper.java b/src/main/java/engineer/nightowl/sonos/api/util/SonosCallbackHelper.java
index c390996..2932fd1 100644
--- a/src/main/java/engineer/nightowl/sonos/api/util/SonosCallbackHelper.java
+++ b/src/main/java/engineer/nightowl/sonos/api/util/SonosCallbackHelper.java
@@ -2,24 +2,28 @@
import engineer.nightowl.sonos.api.SonosApiClient;
import engineer.nightowl.sonos.api.exception.SonosApiClientException;
-import org.apache.http.Header;
-import org.apache.http.HttpMessage;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
-import java.util.Arrays;
import java.util.Base64;
-import java.util.Map;
-import java.util.stream.Collectors;
+import java.util.Optional;
import static java.nio.charset.StandardCharsets.UTF_8;
+import java.net.http.HttpHeaders;
+import java.net.http.HttpResponse;
+
public class SonosCallbackHelper
{
private static final Logger logger = LoggerFactory.getLogger(SonosCallbackHelper.class);
+ private SonosCallbackHelper()
+ {
+ // Empty private constructor
+ }
+
/**
* Verify that the message was signed by Sonos.
*
@@ -29,8 +33,14 @@ public class SonosCallbackHelper
* @return true if the message is cryptographically provable to be from Sonos
* @throws SonosApiClientException if in an unsupported environment
*/
- public static Boolean verifySignature(final Map headers, final String apiKey, final String apiSecret) throws SonosApiClientException
+ public static Boolean verifySignature(final HttpHeaders headers, final String apiKey, final String apiSecret) throws SonosApiClientException
{
+ final Optional sentSignature = headers.firstValue("X-Sonos-Event-Signature");
+ if (!sentSignature.isPresent())
+ {
+ throw new SonosApiClientException("No signature present in header, ending early.");
+ }
+
MessageDigest messageDigest = null;
try
{
@@ -40,42 +50,35 @@ public static Boolean verifySignature(final Map headers, final S
throw new SonosApiClientException("Unsupported execution environment", e);
}
- messageDigest.update(headers.get("X-Sonos-Event-Seq-Id").getBytes(UTF_8));
- messageDigest.update(headers.get("X-Sonos-Namespace").getBytes(UTF_8));
- messageDigest.update(headers.get("X-Sonos-Type").getBytes(UTF_8));
- messageDigest.update(headers.get("X-Sonos-Target-Type").getBytes(UTF_8));
- messageDigest.update(headers.get("X-Sonos-Target-Value").getBytes(UTF_8));
+ if (headers.map().keySet().containsAll(SignatureHeader.getAllHeaders()))
+ {
+ for (SignatureHeader key : SignatureHeader.values())
+ {
+ final Optional value = headers.firstValue(key.getHeaderKey());
+ if (value.isPresent())
+ {
+ messageDigest.update(value.get().getBytes(UTF_8));
+ }
+ }
+ }
+
messageDigest.update(apiKey.getBytes(UTF_8));
- messageDigest.update(apiSecret.getBytes(UTF_8));
+ messageDigest.update(apiSecret.getBytes(UTF_8));
final String signature = Base64.getUrlEncoder().withoutPadding().encodeToString(messageDigest.digest());
logger.debug("Verifying signature: {}", signature);
- return signature.equals(headers.get("X-Sonos-Event-Signature"));
- }
-
- public static Boolean verifySignature(final Map headers, final SonosApiClient apiClient) throws SonosApiClientException
- {
- return SonosCallbackHelper.verifySignature(headers, apiClient.getConfiguration().getApiKey(), apiClient.getConfiguration().getApiSecret());
- }
-
- public static Boolean verifySignature(final HttpMessage message, final SonosApiClient apiClient) throws SonosApiClientException
- {
- return SonosCallbackHelper.verifySignature(message, apiClient.getConfiguration().getApiKey(), apiClient.getConfiguration().getApiSecret());
+ return signature.equals(sentSignature.get());
}
- public static Boolean verifySignature(final HttpMessage message, final String apiKey, final String apiSecret) throws SonosApiClientException
+ public static Boolean verifySignature(final HttpResponse