/*
* Copyright 2015 Open mHealth
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.openmhealth.shim.jawbone;
import com.fasterxml.jackson.databind.JsonNode;
import org.openmhealth.shim.*;
import org.openmhealth.shim.jawbone.mapper.*;
import org.slf4j.Logger;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.http.ResponseEntity;
import org.springframework.security.oauth2.client.OAuth2RestOperations;
import org.springframework.security.oauth2.client.resource.OAuth2ProtectedResourceDetails;
import org.springframework.security.oauth2.client.resource.UserRedirectRequiredException;
import org.springframework.security.oauth2.client.token.AccessTokenRequest;
import org.springframework.security.oauth2.client.token.RequestEnhancer;
import org.springframework.security.oauth2.client.token.grant.code.AuthorizationCodeAccessTokenProvider;
import org.springframework.stereotype.Component;
import org.springframework.util.MultiValueMap;
import org.springframework.util.StringUtils;
import org.springframework.web.client.HttpClientErrorException;
import org.springframework.web.client.HttpServerErrorException;
import org.springframework.web.util.UriComponentsBuilder;
import java.time.OffsetDateTime;
import java.util.Arrays;
import java.util.List;
import static java.util.Collections.singletonList;
import static org.slf4j.LoggerFactory.getLogger;
/**
* Encapsulates parameters specific to the Jawbone API and processes requests for Jawbone data from shimmer.
*
* @author Danilo Bonilla
* @author Chris Schaefbauer
*/
@Component
@ConfigurationProperties(prefix = "openmhealth.shim.jawbone")
public class JawboneShim extends OAuth2ShimBase {
public static final String SHIM_KEY = "jawbone";
private static final String DATA_URL = "https://jawbone.com/nudge/api/v.1.1/users/@me/";
private static final String AUTHORIZE_URL = "https://jawbone.com/auth/oauth2/auth";
private static final String TOKEN_URL = "https://jawbone.com/auth/oauth2/token";
public static final List<String> JAWBONE_SCOPES = Arrays.asList(
"extended_read", "weight_read", "heartrate_read", "meal_read", "move_read", "sleep_read");
private static final Logger logger = getLogger(JawboneShim.class);
@Autowired
public JawboneShim(ApplicationAccessParametersRepo applicationParametersRepo,
AuthorizationRequestParametersRepo authorizationRequestParametersRepo,
AccessParametersRepo accessParametersRepo,
ShimServerConfig shimServerConfig1) {
super(applicationParametersRepo, authorizationRequestParametersRepo, accessParametersRepo, shimServerConfig1);
}
@Override
public String getLabel() {
return "Jawbone UP";
}
@Override
public String getShimKey() {
return SHIM_KEY;
}
@Override
public String getBaseAuthorizeUrl() {
return AUTHORIZE_URL;
}
@Override
public String getBaseTokenUrl() {
return TOKEN_URL;
}
@Override
public List<String> getScopes() {
return JAWBONE_SCOPES;
}
public AuthorizationCodeAccessTokenProvider getAuthorizationCodeAccessTokenProvider() {
return new JawboneAuthorizationCodeAccessTokenProvider();
}
@Override
public ShimDataType[] getShimDataTypes() {
return new JawboneDataTypes[] {
JawboneDataTypes.SLEEP, JawboneDataTypes.ACTIVITY, JawboneDataTypes.BODY_MASS_INDEX,
JawboneDataTypes.WEIGHT, JawboneDataTypes.HEART_RATE, JawboneDataTypes.STEPS};
}
public enum JawboneDataTypes implements ShimDataType {
SLEEP("sleeps"),
ACTIVITY("workouts"),
WEIGHT("body_events"),
STEPS("moves"),
BODY_MASS_INDEX("body_events"),
HEART_RATE("heartrates");
private String endPoint;
JawboneDataTypes(String endPoint) {
this.endPoint = endPoint;
}
public String getEndPoint() {
return endPoint;
}
}
protected ResponseEntity<ShimDataResponse> getData(OAuth2RestOperations restTemplate,
ShimDataRequest shimDataRequest) throws ShimException {
final JawboneDataTypes jawboneDataType;
try {
jawboneDataType = JawboneDataTypes.valueOf(
shimDataRequest.getDataTypeKey().trim().toUpperCase());
}
catch (NullPointerException | IllegalArgumentException e) {
throw new ShimException("Null or Invalid data type parameter: "
+ shimDataRequest.getDataTypeKey()
+ " in shimDataRequest, cannot retrieve data.");
}
/*
Jawbone defaults to returning a maximum of 10 entries per request (limit = 10 by default), so
we override the default by specifying an arbitrarily large number as the limit.
*/
long numToReturn = 100_000;
OffsetDateTime today = OffsetDateTime.now();
OffsetDateTime startDateTime = shimDataRequest.getStartDateTime() == null ?
today.minusDays(1) : shimDataRequest.getStartDateTime();
long startTimeInEpochSecond = startDateTime.toEpochSecond();
// We are inclusive of the last day, so we need to add an extra day since we are dealing with start of day,
// and would miss the activities that occurred during the last day within going to midnight of that day
OffsetDateTime endDateTime = shimDataRequest.getEndDateTime() == null ?
today.plusDays(1) : shimDataRequest.getEndDateTime().plusDays(1);
long endTimeInEpochSecond = endDateTime.toEpochSecond();
UriComponentsBuilder uriComponentsBuilder =
UriComponentsBuilder.fromUriString(DATA_URL).path(jawboneDataType.getEndPoint())
.queryParam("start_time", startTimeInEpochSecond).queryParam("end_time", endTimeInEpochSecond)
.queryParam("limit", numToReturn);
ResponseEntity<JsonNode> responseEntity;
try {
responseEntity = restTemplate.getForEntity(uriComponentsBuilder.build().encode().toUri(), JsonNode.class);
}
catch (HttpClientErrorException | HttpServerErrorException e) {
// FIXME figure out how to handle this
logger.error("A request for Jawbone data failed.", e);
throw e;
}
if (shimDataRequest.getNormalize()) {
JawboneDataPointMapper mapper;
switch ( jawboneDataType ) {
case WEIGHT:
mapper = new JawboneBodyWeightDataPointMapper();
break;
case STEPS:
mapper = new JawboneStepCountDataPointMapper();
break;
case BODY_MASS_INDEX:
mapper = new JawboneBodyMassIndexDataPointMapper();
break;
case ACTIVITY:
mapper = new JawbonePhysicalActivityDataPointMapper();
break;
case SLEEP:
mapper = new JawboneSleepDurationDataPointMapper();
break;
case HEART_RATE:
mapper = new JawboneHeartRateDataPointMapper();
break;
default:
throw new UnsupportedOperationException();
}
return ResponseEntity.ok().body(ShimDataResponse
.result(JawboneShim.SHIM_KEY, mapper.asDataPoints(singletonList(responseEntity.getBody()))));
}
else {
return ResponseEntity.ok().body(ShimDataResponse.result(JawboneShim.SHIM_KEY, responseEntity.getBody()));
}
}
@Override
protected String getAuthorizationUrl(UserRedirectRequiredException exception) {
final OAuth2ProtectedResourceDetails resource = getResource();
UriComponentsBuilder uriBuilder = UriComponentsBuilder
.fromUriString(exception.getRedirectUri())
.queryParam("state", exception.getStateKey())
.queryParam("client_id", resource.getClientId())
.queryParam("response_type", "code")
.queryParam("scope", StringUtils.collectionToDelimitedString(resource.getScope(), " "))
.queryParam("redirect_uri", getCallbackUrl());
return uriBuilder.build().encode().toUriString();
}
/**
* Simple overrides to base spring class from oauth.
*/
public class JawboneAuthorizationCodeAccessTokenProvider extends AuthorizationCodeAccessTokenProvider {
public JawboneAuthorizationCodeAccessTokenProvider() {
this.setTokenRequestEnhancer(new JawboneTokenRequestEnhancer());
}
@Override
protected HttpMethod getHttpMethod() {
return HttpMethod.GET;
}
}
/**
* Adds jawbone required parameters to authorization token requests.
*/
private class JawboneTokenRequestEnhancer implements RequestEnhancer {
@Override
public void enhance(AccessTokenRequest request,
OAuth2ProtectedResourceDetails resource,
MultiValueMap<String, String> form, HttpHeaders headers) {
form.set("client_id", resource.getClientId());
form.set("client_secret", resource.getClientSecret());
}
}
}