Skip to content

Commit

Permalink
Merge branch 'release/0.6.0'
Browse files Browse the repository at this point in the history
  • Loading branch information
emersonf committed Oct 6, 2017
2 parents df1c29c + 390e61e commit 0c4c3e4
Show file tree
Hide file tree
Showing 127 changed files with 4,120 additions and 23,113 deletions.
29 changes: 17 additions & 12 deletions README.md

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ subprojects {
ext {
javaVersion = 1.8
shimmerVersion = "0.6.0"
omhSchemaSdkVersion = "1.1.0"
omhSchemaSdkVersion = "1.2.1"
}

sourceCompatibility = javaVersion
Expand Down
1 change: 1 addition & 0 deletions shim-server/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ ext {
dependencies {
compile project(":java-shim-sdk")
compile "commons-io:commons-io:2.4"
compile "com.google.guava:guava:23.0"
compile "org.hibernate:hibernate-validator"
compile "org.apache.httpcomponents:httpclient"
compile "org.apache.httpcomponents:httpcore"
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
/*
* Copyright 2017 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;

import org.springframework.http.client.ClientHttpRequest;
import org.springframework.security.oauth2.client.DefaultOAuth2RequestAuthenticator;
import org.springframework.security.oauth2.client.OAuth2ClientContext;
import org.springframework.security.oauth2.client.OAuth2RequestAuthenticator;
import org.springframework.security.oauth2.client.http.AccessTokenRequiredException;
import org.springframework.security.oauth2.client.resource.OAuth2ProtectedResourceDetails;
import org.springframework.security.oauth2.common.OAuth2AccessToken;
import org.springframework.util.StringUtils;


/**
* A customization of {@link DefaultOAuth2RequestAuthenticator} that standardizes the case of the Authorization header
* token type to "Bearer". This is necessary because the default implementation doesn't work for Moves, which serves up
* a "bearer" token but only accepts "Bearer" authorization headers.
*
* @author Dave Syer
* @author Emerson Farrugia
*/
public class CaseStandardizingOAuth2RequestAuthenticator implements OAuth2RequestAuthenticator {

@Override
public void authenticate(OAuth2ProtectedResourceDetails resource, OAuth2ClientContext clientContext,
ClientHttpRequest request) {

OAuth2AccessToken accessToken = clientContext.getAccessToken();
if (accessToken == null) {
throw new AccessTokenRequiredException(resource);
}

String tokenType = accessToken.getTokenType();

if (!StringUtils.hasText(tokenType) || tokenType.equalsIgnoreCase(OAuth2AccessToken.BEARER_TYPE)) {
tokenType = OAuth2AccessToken.BEARER_TYPE; // we'll assume basic bearer token type if none is specified.
}

request.getHeaders().set("Authorization", String.format("%s %s", tokenType, accessToken.getValue()));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -274,6 +274,8 @@ protected OAuth2RestOperations restTemplate(String stateKey, String code) {
new AccessParameterClientTokenServices(accessParametersRepo));
restTemplate.setAccessTokenProvider(tokenProviderChain);

restTemplate.setAuthenticator(new CaseStandardizingOAuth2RequestAuthenticator());

return restTemplate;
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
/*
* Copyright 2017 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;

import java.util.Optional;
import java.util.stream.Stream;


/**
* A set of utility methods to help with {@link Optional} {@link Stream} objects.
*
* @author Emerson Farrugia
*/
public class OptionalStreamSupport {

@SuppressWarnings("OptionalUsedAsFieldOrParameterType")
public static <O> Stream<O> asStream(Optional<O> optional) {

return optional.map(Stream::of).orElseGet(Stream::empty);
}
}
Original file line number Diff line number Diff line change
@@ -1,3 +1,19 @@
/*
* Copyright 2017 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.fitbit.mapper;

import com.fasterxml.jackson.databind.JsonNode;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,8 @@ public class GoogleFitClientSettings extends OAuth2ClientSettings {

private List<String> scopes = Arrays.asList(
"https://www.googleapis.com/auth/fitness.activity.read",
"https://www.googleapis.com/auth/fitness.body.read"
"https://www.googleapis.com/auth/fitness.body.read",
"https://www.googleapis.com/auth/fitness.location.read"
);

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,6 @@
import java.time.ZoneOffset;
import java.util.Map;

import static java.util.Collections.singletonList;
import static org.slf4j.LoggerFactory.getLogger;
import static org.springframework.http.ResponseEntity.ok;

Expand Down Expand Up @@ -114,8 +113,10 @@ public ShimDataType[] getShimDataTypes() {
GoogleFitDataTypes.BODY_HEIGHT,
GoogleFitDataTypes.BODY_WEIGHT,
GoogleFitDataTypes.CALORIES_BURNED,
GoogleFitDataTypes.GEOPOSITION,
GoogleFitDataTypes.HEART_RATE,
GoogleFitDataTypes.PHYSICAL_ACTIVITY,
GoogleFitDataTypes.SPEED,
GoogleFitDataTypes.STEP_COUNT
};
}
Expand All @@ -125,22 +126,21 @@ public enum GoogleFitDataTypes implements ShimDataType {
BODY_HEIGHT("derived:com.google.height:com.google.android.gms:merge_height"),
BODY_WEIGHT("derived:com.google.weight:com.google.android.gms:merge_weight"),
CALORIES_BURNED("derived:com.google.calories.expended:com.google.android.gms:merge_calories_expended"),
GEOPOSITION("derived:com.google.location.sample:com.google.android.gms:merge_location_samples"),
HEART_RATE("derived:com.google.heart_rate.bpm:com.google.android.gms:merge_heart_rate_bpm"),
PHYSICAL_ACTIVITY("derived:com.google.activity.segment:com.google.android.gms:merge_activity_segments"),
SPEED("derived:com.google.speed:com.google.android.gms:merge_speed"),
STEP_COUNT("derived:com.google.step_count.delta:com.google.android.gms:merge_step_deltas");

private final String streamId;

GoogleFitDataTypes(String streamId) {

this.streamId = streamId;
}

public String getStreamId() {

return streamId;
}

}

protected ResponseEntity<ShimDataResponse> getData(OAuth2RestOperations restTemplate,
Expand All @@ -157,31 +157,34 @@ protected ResponseEntity<ShimDataResponse> getData(OAuth2RestOperations restTemp
+ " 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();
long startTimeNanos = (startDateInUTC.toEpochSecond() * 1_000_000_000) + 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();
long endTimeNanos = (endDateInUTC.toEpochSecond() * 1_000_000_000) + 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();
URI uri = UriComponentsBuilder
.fromUriString(DATA_URL)
.pathSegment(googleFitDataType.getStreamId(), "datasets", "{startDate}-{endDate}")
.buildAndExpand(startTimeNanos, endTimeNanos)
.encode()
.toUri();

ResponseEntity<JsonNode> responseEntity;
try {
responseEntity = restTemplate.getForEntity(uriRequest, JsonNode.class);
responseEntity = restTemplate.getForEntity(uri, JsonNode.class);
}
catch (HttpClientErrorException | HttpServerErrorException e) {
// TODO figure out how to handle this
Expand All @@ -190,36 +193,46 @@ protected ResponseEntity<ShimDataResponse> getData(OAuth2RestOperations restTemp
}

if (shimDataRequest.getNormalize()) {
GoogleFitDataPointMapper<?> dataPointMapper;
switch (googleFitDataType) {
case BODY_WEIGHT:
dataPointMapper = new GoogleFitBodyWeightDataPointMapper();
break;
case BODY_HEIGHT:
dataPointMapper = new GoogleFitBodyHeightDataPointMapper();
break;
case PHYSICAL_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();
}
GoogleFitDataPointMapper<?> dataPointMapper = getDataPointMapper(googleFitDataType);

return ok().body(ShimDataResponse.result(GoogleFitShim.SHIM_KEY, dataPointMapper.asDataPoints(
singletonList(responseEntity.getBody()))));
return ok().body(ShimDataResponse
.result(GoogleFitShim.SHIM_KEY, dataPointMapper.asDataPoints(responseEntity.getBody())));
}
else {
return ok().body(ShimDataResponse
.result(GoogleFitShim.SHIM_KEY, responseEntity.getBody()));
}
}

private GoogleFitDataPointMapper<?> getDataPointMapper(GoogleFitDataTypes googleFitDataType) {

switch (googleFitDataType) {
case BODY_HEIGHT:
return new GoogleFitBodyHeightDataPointMapper();

case BODY_WEIGHT:
return new GoogleFitBodyWeightDataPointMapper();

case CALORIES_BURNED:
return new GoogleFitCaloriesBurnedDataPointMapper();

case GEOPOSITION:
return new GoogleFitGeopositionDataPointMapper();

case HEART_RATE:
return new GoogleFitHeartRateDataPointMapper();

case PHYSICAL_ACTIVITY:
return new GoogleFitPhysicalActivityDataPointMapper();

case SPEED:
return new GoogleFitSpeedDataPointMapper();

case STEP_COUNT:
return new GoogleFitStepCountDataPointMapper();

return ok().body(ShimDataResponse.result(GoogleFitShim.SHIM_KEY, responseEntity.getBody()));
default:
throw new UnsupportedOperationException();
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,11 +46,11 @@ public Optional<DataPoint<BodyHeight>> asDataPoint(JsonNode listNode) {
return Optional.empty();
}

BodyHeight.Builder bodyHeightBuilder = new BodyHeight.Builder(new LengthUnitValue(METER, bodyHeightValue));
BodyHeight.Builder measureBuilder = new BodyHeight.Builder(new LengthUnitValue(METER, bodyHeightValue));

setEffectiveTimeFrameIfPresent(bodyHeightBuilder, listNode);
getOptionalTimeFrame(listNode).ifPresent(measureBuilder::setEffectiveTimeFrame);

BodyHeight bodyHeight = bodyHeightBuilder.build();
BodyHeight bodyHeight = measureBuilder.build();
Optional<String> originDataSourceId = asOptionalString(listNode, "originDataSourceId");

return Optional.of(newDataPoint(bodyHeight, originDataSourceId.orElse(null)));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,12 +47,13 @@ public Optional<DataPoint<BodyWeight>> asDataPoint(JsonNode listNode) {
return Optional.empty();
}

BodyWeight.Builder bodyWeightBuilder = new BodyWeight.Builder(new MassUnitValue(KILOGRAM, bodyWeightValue));
setEffectiveTimeFrameIfPresent(bodyWeightBuilder, listNode);
BodyWeight.Builder measureBuilder = new BodyWeight.Builder(new MassUnitValue(KILOGRAM, bodyWeightValue));

getOptionalTimeFrame(listNode).ifPresent(measureBuilder::setEffectiveTimeFrame);

Optional<String> originDataSourceId = asOptionalString(listNode, "originDataSourceId");

BodyWeight bodyWeight = bodyWeightBuilder.build();
BodyWeight bodyWeight = measureBuilder.build();
return Optional.of(newDataPoint(bodyWeight, originDataSourceId.orElse(null)));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,8 @@
package org.openmhealth.shim.googlefit.mapper;

import com.fasterxml.jackson.databind.JsonNode;
import org.openmhealth.schema.domain.omh.CaloriesBurned;
import org.openmhealth.schema.domain.omh.CaloriesBurned2;
import org.openmhealth.schema.domain.omh.DataPoint;
import org.openmhealth.schema.domain.omh.KcalUnitValue;

import java.util.Optional;

Expand All @@ -28,32 +27,29 @@


/**
* A mapper from Google Fit "merged calories expended" endpoint responses
* (derived:com.google.calories.expended:com.google.android.gms:merge_calories_expended) to {@link CaloriesBurned}
* objects.
* A mapper from Google Fit "merged calories expended" endpoint responses (derived:com.google.calories.expended:com.google.android.gms:merge_calories_expended)
* to {@link CaloriesBurned2} objects.
*
* @author Chris Schaefbauer
* @see <a href="https://developers.google.com/fit/rest/v1/data-types">Google Fit Data Type Documentation</a>
*/
public class GoogleFitCaloriesBurnedDataPointMapper extends GoogleFitDataPointMapper<CaloriesBurned> {
public class GoogleFitCaloriesBurnedDataPointMapper extends GoogleFitDataPointMapper<CaloriesBurned2> {

@Override
protected Optional<DataPoint<CaloriesBurned>> asDataPoint(JsonNode listNode) {
protected Optional<DataPoint<CaloriesBurned2>> asDataPoint(JsonNode listNode) {

JsonNode listValueNode = asRequiredNode(listNode, "value");
// TODO isn't this just "value.fpVal"?
double caloriesBurnedValue = asRequiredDouble(listValueNode.get(0), "fpVal");

CaloriesBurned.Builder caloriesBurnedBuilder =
new CaloriesBurned.Builder(new KcalUnitValue(KILOCALORIE, caloriesBurnedValue));
CaloriesBurned2.Builder measureBuilder =
new CaloriesBurned2.Builder(KILOCALORIE.newUnitValue(caloriesBurnedValue), getTimeFrame(listNode));

setEffectiveTimeFrameIfPresent(caloriesBurnedBuilder, listNode);
CaloriesBurned2 caloriesBurned = measureBuilder.build();

CaloriesBurned caloriesBurned = caloriesBurnedBuilder.build();
Optional<String> originDataSourceId = asOptionalString(listNode, "originDataSourceId");

// Google Fit calories burned endpoint returns calories burned by basal metabolic rate (BMR), however these
// are not activity related calories burned so we do not create a datapoint for values from this source
// are not activity related calories burned so we do not create a data point for values from this source
if (originDataSourceId.isPresent()) {
if (originDataSourceId.get().contains("bmr")) {
return Optional.empty();
Expand Down
Loading

0 comments on commit 0c4c3e4

Please sign in to comment.