Java tutorial
/* * 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.googlefit; import com.fasterxml.jackson.databind.JsonNode; import org.openmhealth.shim.*; import org.openmhealth.shim.googlefit.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.OAuth2AccessDeniedException; 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.security.oauth2.common.DefaultOAuth2AccessToken; import org.springframework.security.oauth2.common.OAuth2AccessToken; import org.springframework.security.oauth2.common.OAuth2RefreshToken; 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.net.URI; import java.time.LocalDate; import java.time.OffsetDateTime; import java.time.ZoneOffset; import java.util.Arrays; import java.util.List; import static java.util.Collections.singletonList; import static org.slf4j.LoggerFactory.getLogger; import static org.springframework.http.ResponseEntity.ok; /** * Encapsulates parameters specific to the Google Fit REST API and processes requests for Google Fit data from shimmer. * * @author Eric Jain * @author Chris Schaefbauer */ @Component @ConfigurationProperties(prefix = "openmhealth.shim.googlefit") public class GoogleFitShim extends OAuth2ShimBase { private static final Logger logger = getLogger(GoogleFitShim.class); public static final String SHIM_KEY = "googlefit"; private static final String DATA_URL = "https://www.googleapis.com/fitness/v1/users/me/dataSources"; private static final String AUTHORIZE_URL = "https://accounts.google.com/o/oauth2/auth"; private static final String TOKEN_URL = "https://accounts.google.com/o/oauth2/token"; public static final List<String> GOOGLE_FIT_SCOPES = Arrays.asList( "https://www.googleapis.com/auth/userinfo.email", "https://www.googleapis.com/auth/fitness.activity.read", "https://www.googleapis.com/auth/fitness.body.read"); @Autowired public GoogleFitShim(ApplicationAccessParametersRepo applicationParametersRepo, AuthorizationRequestParametersRepo authorizationRequestParametersRepo, AccessParametersRepo accessParametersRepo, ShimServerConfig shimServerConfig) { super(applicationParametersRepo, authorizationRequestParametersRepo, accessParametersRepo, shimServerConfig); } @Override public String getLabel() { return "Google Fit"; } @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 GOOGLE_FIT_SCOPES; } public AuthorizationCodeAccessTokenProvider getAuthorizationCodeAccessTokenProvider() { return new GoogleAuthorizationCodeAccessTokenProvider(); } @Override public ShimDataType[] getShimDataTypes() { return new GoogleFitDataTypes[] { GoogleFitDataTypes.ACTIVITY, GoogleFitDataTypes.BODY_HEIGHT, GoogleFitDataTypes.BODY_WEIGHT, GoogleFitDataTypes.HEART_RATE, GoogleFitDataTypes.STEP_COUNT, GoogleFitDataTypes.CALORIES_BURNED }; } public enum GoogleFitDataTypes implements ShimDataType { ACTIVITY("derived:com.google.activity.segment:com.google.android.gms:merge_activity_segments"), BODY_HEIGHT( "derived:com.google.height:com.google.android.gms:merge_height"), BODY_WEIGHT( "derived:com.google.weight:com.google.android.gms:merge_weight"), HEART_RATE( "derived:com.google.heart_rate.bpm:com.google.android.gms:merge_heart_rate_bpm"), STEP_COUNT( "derived:com.google.step_count.delta:com.google.android.gms:merge_step_deltas"), CALORIES_BURNED( "derived:com.google.calories.expended:com.google.android.gms:merge_calories_expended"); private final String streamId; GoogleFitDataTypes(String streamId) { this.streamId = streamId; } public String getStreamId() { return streamId; } } protected ResponseEntity<ShimDataResponse> getData(OAuth2RestOperations restTemplate, ShimDataRequest shimDataRequest) throws ShimException { final GoogleFitDataTypes googleFitDataType; try { googleFitDataType = GoogleFitDataTypes.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."); } OffsetDateTime todayInUTC = LocalDate.now().atStartOfDay().atOffset(ZoneOffset.UTC); OffsetDateTime startDateInUTC = shimDataRequest.getStartDateTime() == null ? todayInUTC.minusDays(1) : shimDataRequest.getStartDateTime(); long startTimeNanos = (startDateInUTC.toEpochSecond() * 1000000000) + startDateInUTC.toInstant().getNano(); OffsetDateTime endDateInUTC = shimDataRequest.getEndDateTime() == null ? todayInUTC.plusDays(1) : shimDataRequest.getEndDateTime().plusDays(1); // We are inclusive of the last day, so add 1 day to get // the end of day on the last day, which captures the // entire last day long endTimeNanos = (endDateInUTC.toEpochSecond() * 1000000000) + endDateInUTC.toInstant().getNano(); UriComponentsBuilder uriBuilder = UriComponentsBuilder.fromUriString(DATA_URL) .pathSegment(googleFitDataType.getStreamId(), "datasets", "{startDate}-{endDate}"); // TODO: Add limits back into the request once Google has fixed the 'limit' query parameter and paging URI uriRequest = uriBuilder.buildAndExpand(startTimeNanos, endTimeNanos).encode().toUri(); ResponseEntity<JsonNode> responseEntity; try { responseEntity = restTemplate.getForEntity(uriRequest, JsonNode.class); } catch (HttpClientErrorException | HttpServerErrorException e) { // TODO figure out how to handle this logger.error("A request for Google Fit data failed.", e); throw e; } if (shimDataRequest.getNormalize()) { GoogleFitDataPointMapper<?> dataPointMapper; switch (googleFitDataType) { case BODY_WEIGHT: dataPointMapper = new GoogleFitBodyWeightDataPointMapper(); break; case BODY_HEIGHT: dataPointMapper = new GoogleFitBodyHeightDataPointMapper(); break; case ACTIVITY: dataPointMapper = new GoogleFitPhysicalActivityDataPointMapper(); break; case STEP_COUNT: dataPointMapper = new GoogleFitStepCountDataPointMapper(); break; case HEART_RATE: dataPointMapper = new GoogleFitHeartRateDataPointMapper(); break; case CALORIES_BURNED: dataPointMapper = new GoogleFitCaloriesBurnedDataPointMapper(); break; default: throw new UnsupportedOperationException(); } return ok().body(ShimDataResponse.result(GoogleFitShim.SHIM_KEY, dataPointMapper.asDataPoints(singletonList(responseEntity.getBody())))); } else { return ok().body(ShimDataResponse.result(GoogleFitShim.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("access_type", "offline") .queryParam("approval_prompt", "force") .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 GoogleAuthorizationCodeAccessTokenProvider extends AuthorizationCodeAccessTokenProvider { public GoogleAuthorizationCodeAccessTokenProvider() { this.setTokenRequestEnhancer(new GoogleTokenRequestEnhancer()); } @Override protected HttpMethod getHttpMethod() { return HttpMethod.POST; } @Override public OAuth2AccessToken refreshAccessToken(OAuth2ProtectedResourceDetails resource, OAuth2RefreshToken refreshToken, AccessTokenRequest request) throws UserRedirectRequiredException, OAuth2AccessDeniedException { OAuth2AccessToken accessToken = super.refreshAccessToken(resource, refreshToken, request); // Google does not replace refresh tokens, so we need to hold on to the existing refresh token... if (accessToken.getRefreshToken() == null) { ((DefaultOAuth2AccessToken) accessToken).setRefreshToken(refreshToken); } return accessToken; } } /** * Adds parameters required by Google to authorization token requests. */ private class GoogleTokenRequestEnhancer 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()); if (request.getStateKey() != null) { form.set("redirect_uri", getCallbackUrl()); } } } }