diff --git a/README.md b/README.md index bd39dd21..9813c09f 100644 --- a/README.md +++ b/README.md @@ -11,12 +11,12 @@ We currently support the following APIs * [Misfit](http://misfit.com/) * [RunKeeper](https://runkeeper.com/index) * [Withings](http://www.withings.com/) +* [iHealth](http://www.ihealthlabs.com/) And the following APIs are in the works * [FatSecret](https://www.fatsecret.com/) * [Ginsberg](https://www.ginsberg.io/) -* [iHealth](http://www.ihealthlabs.com/) * [Strava](https://www.strava.com/) This README should have everything you need to get started. If you have any questions, feel free to [open an issue](https://github.com/openmhealth/shimmer/issues), [email us](mailto://admin@openmhealth.org), [post on our form](https://groups.google.com/forum/#!forum/omh-developers), or [visit our website](http://www.openmhealth.org/documentation/#/data-providers/get-started). @@ -122,6 +122,11 @@ You need to obtain client credentials for any shim you'd like to use. These cred * [Misfit](https://build.misfit.com/) * [RunKeeper](http://developer.runkeeper.com/healthgraph) ([application management portal](http://runkeeper.com/partner)) * [Withings](http://oauth.withings.com/api) +* [iHealth](http://developer.ihealthlabs.com/index.htm) (see below for setting up special iHealth application specific credentials) + +> If you are using the iHealth shim, you must uncomment and replace the SC and SV values for each endpoint in the `iHealth:serialValues` map in the `application.yaml` file. +These values are uniquely associated with each project you have and can be found in your project details on the [application management page](http://developer.ihealthlabs.com/developermanagepage.htm) +of the iHealth developers site. If any of the links are incorrect or out of date, please [submit an issue](https://github.com/openmhealth/shimmer/issues) to let us know. @@ -246,7 +251,14 @@ The currently supported shims are: | withings | steps4 | [omh:step-count](http://www.openmhealth.org/documentation/#/schema-docs/schema-library/schemas/omh_step-count) | | withings | calories4 | [omh:calories-burned](http://www.openmhealth.org/documentation/#/schema-docs/schema-library/schemas/omh_calories-burned) | | withings | sleep5 | [omh:sleep-duration](http://www.openmhealth.org/documentation/#/schema-docs/schema-library/schemas/omh_sleep-duration) | - +| ihealth | physical_activity | [omh:physical-activity](http://www.openmhealth.org/documentation/#/schema-docs/schema-library/schemas/omh_physical-activity) | +| ihealth | blood_glucose | [omh:blood-glucose](http://www.openmhealth.org/documentation/#/schema-docs/schema-library/schemas/omh_blood-glucose) | +| ihealth | blood_pressure | [omh:blood-pressure](http://www.openmhealth.org/documentation/#/schema-docs/schema-library/schemas/omh_blood-pressure) | +| ihealth | body_weight | [omh:body-weight](http://www.openmhealth.org/documentation/#/schema-docs/schema-library/schemas/omh_body-weight) | +| ihealth | body_mass_index | [omh:body-mass-index](http://www.openmhealth.org/documentation/#/schema-docs/schema-library/schemas/omh_body-mass-index) | +| ihealth | heart_rate | [omh:heart-rate](http://www.openmhealth.org/documentation/#/schema-docs/schema-library/schemas/omh_heart-rate) | +| ihealth | step_count | [omh:step-count](http://www.openmhealth.org/documentation/#/schema-docs/schema-library/schemas/omh_step-count) | +| ihealth | sleep_duration | [omh:sleep-duration](http://www.openmhealth.org/documentation/#/schema-docs/schema-library/schemas/omh_sleep-duration) | 1 *The Fitbit API does not provide time zone information for the data points it returns. Furthermore, it is not possible to infer the time zone from any of the information provided. Because Open mHealth schemas require timestamps to have a time zone, we need to assign a time zone to timestamps. We set the time zone of all timestamps to UTC for consistency, even if the data may not have occurred in that time zone. This means that unless the event actually occurred in UTC, the timestamps will be incorrect. Please consider this when working with data normalized into OmH schemas that are retrieved from the Fitbit shim. We will fix this as soon as Fitbit makes changes to their API to provide time zone information.* diff --git a/shim-server/src/main/java/org/openmhealth/shim/OAuth2ShimBase.java b/shim-server/src/main/java/org/openmhealth/shim/OAuth2ShimBase.java index 99bf3ce8..7e9dc346 100644 --- a/shim-server/src/main/java/org/openmhealth/shim/OAuth2ShimBase.java +++ b/shim-server/src/main/java/org/openmhealth/shim/OAuth2ShimBase.java @@ -17,6 +17,7 @@ package org.openmhealth.shim; +import org.openmhealth.shim.common.mapper.JsonNodeMappingException; import org.springframework.data.domain.Sort; import org.springframework.http.ResponseEntity; import org.springframework.security.oauth2.client.DefaultOAuth2ClientContext; @@ -158,7 +159,13 @@ public AuthorizationResponse handleAuthorizationResponse(HttpServletRequest serv accessParameters.setStateKey(state); accessParametersRepo.save(accessParameters); - trigger(restTemplate, getTriggerDataRequest()); + try{ + trigger(restTemplate, getTriggerDataRequest()); + } + catch(JsonNodeMappingException e){ + // In this case authentication may have succeeded, but the data request may have failed so we + // should not fail. We should check and see if authentication succeeded in subsequent lines. + } /** * By this line we will have an approved access token or diff --git a/shim-server/src/main/java/org/openmhealth/shim/ihealth/IHealthShim.java b/shim-server/src/main/java/org/openmhealth/shim/ihealth/IHealthShim.java new file mode 100644 index 00000000..31250005 --- /dev/null +++ b/shim-server/src/main/java/org/openmhealth/shim/ihealth/IHealthShim.java @@ -0,0 +1,419 @@ +/* + * 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.ihealth; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.google.common.base.Joiner; +import com.google.common.base.Preconditions; +import com.google.common.collect.ImmutableMap; +import org.openmhealth.shim.*; +import org.openmhealth.shim.ihealth.mapper.*; +import org.slf4j.Logger; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpMethod; +import org.springframework.http.ResponseEntity; +import org.springframework.http.client.ClientHttpResponse; +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.security.oauth2.client.token.grant.code.AuthorizationCodeResourceDetails; +import org.springframework.security.oauth2.common.AuthenticationScheme; +import org.springframework.security.oauth2.common.DefaultOAuth2AccessToken; +import org.springframework.security.oauth2.common.DefaultOAuth2RefreshToken; +import org.springframework.security.oauth2.common.OAuth2AccessToken; +import org.springframework.security.oauth2.common.util.SerializationUtils; +import org.springframework.stereotype.Component; +import org.springframework.util.MultiValueMap; +import org.springframework.web.client.HttpClientErrorException; +import org.springframework.web.client.HttpServerErrorException; +import org.springframework.web.client.ResponseExtractor; +import org.springframework.web.util.UriComponentsBuilder; + +import java.io.IOException; +import java.net.URI; +import java.time.OffsetDateTime; +import java.util.Arrays; +import java.util.Date; +import java.util.List; +import java.util.Map; + +import static com.google.common.collect.Lists.newArrayList; +import static java.util.Collections.singletonList; +import static org.openmhealth.shim.ihealth.IHealthShim.IHealthDataTypes.*; +import static org.slf4j.LoggerFactory.getLogger; + + +/** + * Encapsulates parameters specific to the iHealth REST API and processes requests made of shimmer for iHealth data. + * + * @author Chris Schaefbauer + * @author Emerson Farrugia + */ +@Component +@EnableConfigurationProperties +@ConfigurationProperties(prefix = "openmhealth.shim.ihealth") +public class IHealthShim extends OAuth2ShimBase { + + public static final String SHIM_KEY = "ihealth"; + + private static final String API_URL = "https://api.ihealthlabs.com:8443/openapiv2/"; + + private static final String AUTHORIZE_URL = "https://api.ihealthlabs.com:8443/OpenApiV2/OAuthv2/userauthorization/"; + + private static final String TOKEN_URL = AUTHORIZE_URL; + + public static final List IHEALTH_SCOPES = Arrays.asList("OpenApiActivity", "OpenApiBP", "OpenApiSleep", + "OpenApiWeight", "OpenApiBG", "OpenApiSpO2", "OpenApiUserInfo", "OpenApiFood", "OpenApiSport"); + + private static final Logger logger = getLogger(IHealthShim.class); + + @Autowired + public IHealthShim(ApplicationAccessParametersRepo applicationParametersRepo, + AuthorizationRequestParametersRepo authorizationRequestParametersRepo, + AccessParametersRepo accessParametersRepo, + ShimServerConfig shimServerConfig) { + super(applicationParametersRepo, authorizationRequestParametersRepo, accessParametersRepo, shimServerConfig); + } + + @Override + public String getLabel() { + return "iHealth"; + } + + @Override + public String getShimKey() { + return SHIM_KEY; + } + + @Override + public String getBaseAuthorizeUrl() { + return AUTHORIZE_URL; + } + + @Override + public String getBaseTokenUrl() { + return TOKEN_URL; + } + + @Override + public List getScopes() { + return IHEALTH_SCOPES; + } + + @Override + public AuthorizationCodeAccessTokenProvider getAuthorizationCodeAccessTokenProvider() { + return new IHealthAuthorizationCodeAccessTokenProvider(); + } + + @Override + public ShimDataType[] getShimDataTypes() { + return new ShimDataType[] { + PHYSICAL_ACTIVITY, + BLOOD_GLUCOSE, + BLOOD_PRESSURE, + BODY_WEIGHT, + BODY_MASS_INDEX, + HEART_RATE, + STEP_COUNT, + SLEEP_DURATION + }; + } + + /** + * Map of values auto-configured from the application.yaml. + */ + Map serialValues; + + public Map getSerialValues() { + + return serialValues; + } + + public void setSerialValues(Map serialValues) { + + this.serialValues = serialValues; + } + + public enum IHealthDataTypes implements ShimDataType { + + PHYSICAL_ACTIVITY(singletonList("sport.json")), + BLOOD_GLUCOSE(singletonList("glucose.json")), + BLOOD_PRESSURE(singletonList("bp.json")), + BODY_WEIGHT(singletonList("weight.json")), + BODY_MASS_INDEX(singletonList("weight.json")), + HEART_RATE(newArrayList("bp.json", "spo2.json")), + STEP_COUNT(singletonList("activity.json")), + SLEEP_DURATION(singletonList("sleep.json")); + + private List endPoint; + + IHealthDataTypes(List endPoint) { + + this.endPoint = endPoint; + } + + public List getEndPoint() { + return endPoint; + } + + } + + @Override + protected ResponseEntity getData(OAuth2RestOperations restTemplate, + ShimDataRequest shimDataRequest) throws ShimException { + + final IHealthDataTypes dataType; + try { + dataType = 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 now = OffsetDateTime.now(); + OffsetDateTime startDate = shimDataRequest.getStartDateTime() == null ? + now.minusDays(1) : shimDataRequest.getStartDateTime(); + OffsetDateTime endDate = shimDataRequest.getEndDateTime() == null ? + now.plusDays(1) : shimDataRequest.getEndDateTime(); + + /* + The physical activity point handles start and end datetimes differently than the other endpoints. It + requires use to include the range until the beginning of the next day. + */ + if (dataType == PHYSICAL_ACTIVITY) { + + endDate = endDate.plusDays(1); + } + + // SC and SV values are client-based keys that are unique to each endpoint within a project + String scValue = getScValue(); + List svValues = getSvValues(dataType); + + List responseEntities = newArrayList(); + + int i = 0; + + // We iterate because one of the measures (Heart rate) comes from multiple endpoints, so we submit + // requests to each of these endpoints, map the responses separately and then combine them + for (String endPoint : dataType.getEndPoint()) { + + UriComponentsBuilder uriBuilder = UriComponentsBuilder.fromUriString(API_URL); + + // Need to use a dummy userId if we haven't authenticated yet. This is the case where we are using + // getData to trigger Spring to conduct the OAuth exchange + String userId = "uk"; + + if (shimDataRequest.getAccessParameters() != null) { + + OAuth2AccessToken token = + SerializationUtils.deserialize(shimDataRequest.getAccessParameters().getSerializedToken()); + + userId = Preconditions.checkNotNull((String) token.getAdditionalInformation().get("UserID")); + uriBuilder.queryParam("access_token", token.getValue()); + } + + uriBuilder.path("/user/") + .path(userId + "/") + .path(endPoint) + .queryParam("client_id", restTemplate.getResource().getClientId()) + .queryParam("client_secret", restTemplate.getResource().getClientSecret()) + .queryParam("start_time", startDate.toEpochSecond()) + .queryParam("end_time", endDate.toEpochSecond()) + .queryParam("locale", "default") + .queryParam("sc", scValue) + .queryParam("sv", svValues.get(i)); + + ResponseEntity responseEntity; + + try { + URI url = uriBuilder.build().encode().toUri(); + responseEntity = restTemplate.getForEntity(url, JsonNode.class); + } + catch (HttpClientErrorException | HttpServerErrorException e) { + // FIXME figure out how to handle this + logger.error("A request for iHealth data failed.", e); + throw e; + } + + if (shimDataRequest.getNormalize()) { + + IHealthDataPointMapper mapper; + + switch ( dataType ) { + + case PHYSICAL_ACTIVITY: + mapper = new IHealthPhysicalActivityDataPointMapper(); + break; + case BLOOD_GLUCOSE: + mapper = new IHealthBloodGlucoseDataPointMapper(); + break; + case BLOOD_PRESSURE: + mapper = new IHealthBloodPressureDataPointMapper(); + break; + case BODY_WEIGHT: + mapper = new IHealthBodyWeightDataPointMapper(); + break; + case BODY_MASS_INDEX: + mapper = new IHealthBodyMassIndexDataPointMapper(); + break; + case STEP_COUNT: + mapper = new IHealthStepCountDataPointMapper(); + break; + case SLEEP_DURATION: + mapper = new IHealthSleepDurationDataPointMapper(); + break; + case HEART_RATE: + // there are two different mappers for heart rate because the data can come from two endpoints + if (endPoint == "bp.json") { + mapper = new IHealthBloodPressureEndpointHeartRateDataPointMapper(); + break; + } + else if (endPoint == "spo2.json") { + mapper = new IHealthBloodOxygenEndpointHeartRateDataPointMapper(); + break; + } + default: + throw new UnsupportedOperationException(); + } + + responseEntities.addAll(mapper.asDataPoints(singletonList(responseEntity.getBody()))); + + } + else { + responseEntities.add(responseEntity.getBody()); + } + + i++; + + } + + return ResponseEntity.ok().body( + ShimDataResponse.result(SHIM_KEY, responseEntities)); + } + + private String getScValue() { + + return serialValues.get("SC"); + } + + private List getSvValues(IHealthDataTypes dataType) { + + switch ( dataType ) { + case PHYSICAL_ACTIVITY: + return singletonList(serialValues.get("sportSV")); + case BODY_WEIGHT: + return singletonList(serialValues.get("weightSV")); + case BODY_MASS_INDEX: + return singletonList(serialValues.get("weightSV")); // body mass index comes from the weight endpoint + case BLOOD_PRESSURE: + return singletonList(serialValues.get("bloodPressureSV")); + case BLOOD_GLUCOSE: + return singletonList(serialValues.get("bloodGlucoseSV")); + case STEP_COUNT: + return singletonList(serialValues.get("activitySV")); + case SLEEP_DURATION: + return singletonList(serialValues.get("sleepSV")); + case HEART_RATE: + return newArrayList(serialValues.get("bloodPressureSV"), serialValues.get("spo2SV")); + default: + throw new UnsupportedOperationException(); + } + } + + @Override + public OAuth2ProtectedResourceDetails getResource() { + AuthorizationCodeResourceDetails resource = (AuthorizationCodeResourceDetails) super.getResource(); + resource.setAuthenticationScheme(AuthenticationScheme.none); + return resource; + } + + @Override + protected String getAuthorizationUrl(UserRedirectRequiredException exception) { + final OAuth2ProtectedResourceDetails resource = getResource(); + + UriComponentsBuilder callBackUriBuilder = UriComponentsBuilder.fromUriString(getCallbackUrl()) + .queryParam("state", exception.getStateKey()); + + UriComponentsBuilder authorizationUriBuilder = UriComponentsBuilder.fromUriString(exception.getRedirectUri()) + .queryParam("client_id", resource.getClientId()) + .queryParam("response_type", "code") + .queryParam("APIName", Joiner.on(' ').join(resource.getScope())) + .queryParam("redirect_uri", callBackUriBuilder.build().toString()); + + return authorizationUriBuilder.build().encode().toString(); + } + + public class IHealthAuthorizationCodeAccessTokenProvider extends AuthorizationCodeAccessTokenProvider { + + public IHealthAuthorizationCodeAccessTokenProvider() { + this.setTokenRequestEnhancer(new RequestEnhancer() { + + @Override + public void enhance(AccessTokenRequest request, + OAuth2ProtectedResourceDetails resource, + MultiValueMap form, HttpHeaders headers) { + + form.set("client_id", resource.getClientId()); + form.set("client_secret", resource.getClientSecret()); + form.set("redirect_uri", getCallbackUrl()); + form.set("state", request.getStateKey()); + } + }); + } + + @Override + protected HttpMethod getHttpMethod() { + return HttpMethod.GET; + } + + @Override + protected ResponseExtractor getResponseExtractor() { + return new ResponseExtractor() { + + @Override + public OAuth2AccessToken extractData(ClientHttpResponse response) throws IOException { + + JsonNode node = new ObjectMapper().readTree(response.getBody()); + String token = Preconditions + .checkNotNull(node.path("AccessToken").textValue(), "Missing access token: %s", node); + String refreshToken = Preconditions + .checkNotNull(node.path("RefreshToken").textValue(), "Missing refresh token: %s" + node); + String userId = + Preconditions.checkNotNull(node.path("UserID").textValue(), "Missing UserID: %s", node); + long expiresIn = node.path("Expires").longValue() * 1000; + Preconditions.checkArgument(expiresIn > 0, "Missing Expires: %s", node); + + DefaultOAuth2AccessToken accessToken = new DefaultOAuth2AccessToken(token); + accessToken.setExpiration(new Date(System.currentTimeMillis() + expiresIn)); + accessToken.setRefreshToken(new DefaultOAuth2RefreshToken(refreshToken)); + accessToken.setAdditionalInformation(ImmutableMap.of("UserID", userId)); + return accessToken; + } + }; + } + } +} diff --git a/shim-server/src/main/java/org/openmhealth/shim/ihealth/IHealthShim.java.txt b/shim-server/src/main/java/org/openmhealth/shim/ihealth/IHealthShim.java.txt deleted file mode 100644 index 1815fefe..00000000 --- a/shim-server/src/main/java/org/openmhealth/shim/ihealth/IHealthShim.java.txt +++ /dev/null @@ -1,414 +0,0 @@ -package org.openmhealth.shim.ihealth; - -import java.io.IOException; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Date; -import java.util.HashMap; -import java.util.List; -import java.util.Map; - -import org.joda.time.DateTime; -import org.joda.time.DateTimeZone; -import org.openmhealth.schema.pojos.Activity; -import org.openmhealth.schema.pojos.BloodGlucose; -import org.openmhealth.schema.pojos.BloodPressure; -import org.openmhealth.schema.pojos.BodyWeight; -import org.openmhealth.schema.pojos.DataPoint; -import org.openmhealth.schema.pojos.HeartRate; -import org.openmhealth.schema.pojos.SleepDuration; -import org.openmhealth.schema.pojos.StepCount; -import org.openmhealth.schema.pojos.build.ActivityBuilder; -import org.openmhealth.schema.pojos.build.BloodGlucoseBuilder; -import org.openmhealth.schema.pojos.build.BloodPressureBuilder; -import org.openmhealth.schema.pojos.build.BodyWeightBuilder; -import org.openmhealth.schema.pojos.build.HeartRateBuilder; -import org.openmhealth.schema.pojos.build.SleepDurationBuilder; -import org.openmhealth.schema.pojos.build.StepCountBuilder; -import org.openmhealth.schema.pojos.generic.DurationUnitValue; -import org.openmhealth.shim.AccessParametersRepo; -import org.openmhealth.shim.ApplicationAccessParametersRepo; -import org.openmhealth.shim.AuthorizationRequestParametersRepo; -import org.openmhealth.shim.OAuth2ShimBase; -import org.openmhealth.shim.ShimDataRequest; -import org.openmhealth.shim.ShimDataResponse; -import org.openmhealth.shim.ShimDataType; -import org.openmhealth.shim.ShimException; -import org.openmhealth.shim.ShimServerConfig; -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.HttpStatus; -import org.springframework.http.ResponseEntity; -import org.springframework.http.client.ClientHttpResponse; -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.security.oauth2.client.token.grant.code.AuthorizationCodeResourceDetails; -import org.springframework.security.oauth2.common.AuthenticationScheme; -import org.springframework.security.oauth2.common.DefaultOAuth2AccessToken; -import org.springframework.security.oauth2.common.DefaultOAuth2RefreshToken; -import org.springframework.security.oauth2.common.OAuth2AccessToken; -import org.springframework.security.oauth2.common.util.SerializationUtils; -import org.springframework.stereotype.Component; -import org.springframework.util.MultiValueMap; -import org.springframework.web.client.ResponseExtractor; -import com.fasterxml.jackson.core.JsonParser; -import com.fasterxml.jackson.databind.DeserializationContext; -import com.fasterxml.jackson.databind.JsonDeserializer; -import com.fasterxml.jackson.databind.JsonNode; -import com.fasterxml.jackson.databind.ObjectMapper; -import com.fasterxml.jackson.databind.module.SimpleModule; -import com.google.common.base.Joiner; -import com.google.common.base.Preconditions; -import com.google.common.base.Strings; -import com.google.common.collect.ImmutableMap; - -@Component -@ConfigurationProperties(prefix = "openmhealth.shim.ihealth") -public class IHealthShim extends OAuth2ShimBase { - - public static final String SHIM_KEY = "ihealth"; - - private static final String API_URL = "https://api.ihealthlabs.com:8443/openapiv2"; - - private static final String AUTHORIZE_URL = "https://api.ihealthlabs.com:8443/OpenApiV2/OAuthv2/userauthorization/"; - - private static final String TOKEN_URL = AUTHORIZE_URL; - - public static final List IHEALTH_SCOPES = Arrays.asList("OpenApiActivity", "OpenApiBP", "OpenApiSleep", - "OpenApiWeight", "OpenApiBG", "OpenApiSpO2", "OpenApiUserInfo", "OpenApiFood", "OpenApiSport"); - - @Autowired - public IHealthShim(ApplicationAccessParametersRepo applicationParametersRepo, - AuthorizationRequestParametersRepo authorizationRequestParametersRepo, - AccessParametersRepo accessParametersRepo, - ShimServerConfig shimServerConfig) { - super(applicationParametersRepo, authorizationRequestParametersRepo, accessParametersRepo, shimServerConfig); - } - - @Override - public String getLabel() { - return "iHealth"; - } - - @Override - public String getShimKey() { - return SHIM_KEY; - } - - @Override - public String getBaseAuthorizeUrl() { - return AUTHORIZE_URL; - } - - @Override - public String getBaseTokenUrl() { - return TOKEN_URL; - } - - @Override - public List getScopes() { - return IHEALTH_SCOPES; - } - - @Override - public AuthorizationCodeAccessTokenProvider getAuthorizationCodeAccessTokenProvider() { - return new IHealthAuthorizationCodeAccessTokenProvider(); - } - - @Override - public ShimDataType[] getShimDataTypes() { - return new ShimDataType[] { - IHealthDataTypes.ACTIVITY, - IHealthDataTypes.BLOOD_GLUCOSE, - IHealthDataTypes.BLOOD_PRESSURE, - IHealthDataTypes.BODY_WEIGHT, - IHealthDataTypes.SLEEP, - IHealthDataTypes.STEP_COUNT - }; - } - - public enum IHealthDataTypes implements ShimDataType { - - ACTIVITY("sport", new JsonDeserializer() { - @Override - public ShimDataResponse deserialize(JsonParser jsonParser, DeserializationContext deserializationContext) - throws IOException { - - JsonNode responseNode = jsonParser.getCodec().readTree(jsonParser); - if (responseNode.path("SPORTDataList").size() == 0) { - return ShimDataResponse.empty(IHealthShim.SHIM_KEY); - } - List activities = new ArrayList<>(); - for (JsonNode node : responseNode.path("SPORTDataList")) { - activities.add(new ActivityBuilder() - .withStartAndEnd(dateTimeValue(node.path("SportStartTime")), dateTimeValue(node.path("SportEndTime"))) - .setActivityName(textValue(node.path("SportName"))).build()); - } - Map results = new HashMap<>(); - results.put(Activity.SCHEMA_ACTIVITY, activities); - return ShimDataResponse.result(IHealthShim.SHIM_KEY, results); - } - }), - - BLOOD_GLUCOSE("glucose", new JsonDeserializer() { - @Override - public ShimDataResponse deserialize(JsonParser jsonParser, DeserializationContext deserializationContext) - throws IOException { - - JsonNode responseNode = jsonParser.getCodec().readTree(jsonParser); - if (responseNode.path("BGDataList").size() == 0) { - return ShimDataResponse.empty(IHealthShim.SHIM_KEY); - } - List datapoints = new ArrayList<>(); - for (JsonNode node : responseNode.path("BGDataList")) { - datapoints.add(new BloodGlucoseBuilder().setTimeTaken(dateTimeValue(node.path("MDate"))) - .setMgdLValue(node.path("BG").decimalValue()) - .setNotes(textValue(node.path("Note"))).build()); - } - Map results = new HashMap<>(); - results.put(BloodGlucose.SCHEMA_BLOOD_GLUCOSE, datapoints); - return ShimDataResponse.result(IHealthShim.SHIM_KEY, results); - } - }), - - BLOOD_PRESSURE("bp", new JsonDeserializer() { - @Override - public ShimDataResponse deserialize(JsonParser jsonParser, DeserializationContext deserializationContext) - throws IOException { - - JsonNode responseNode = jsonParser.getCodec().readTree(jsonParser); - if (responseNode.path("BPDataList").size() == 0) { - return ShimDataResponse.empty(IHealthShim.SHIM_KEY); - } - List bloodPressure = new ArrayList<>(); - List heartRate = new ArrayList<>(); - for (JsonNode node : responseNode.path("BPDataList")) { - DateTime start = dateTimeValue(node.path("MDate")); - bloodPressure.add(new BloodPressureBuilder().setTimeTaken(start) - .setValues(node.path("HP").decimalValue(), node.path("LP").decimalValue()) - .setNotes(textValue(node.path("Note"))).build()); - heartRate.add(new HeartRateBuilder().withTimeTaken(start) - .withRate(node.path("HR").intValue()).build()); - } - Map results = new HashMap<>(); - results.put(BloodPressure.SCHEMA_BLOOD_PRESSURE, bloodPressure); - results.put(HeartRate.SCHEMA_HEART_RATE, heartRate); - return ShimDataResponse.result(IHealthShim.SHIM_KEY, results); - } - }), - - BODY_WEIGHT("weight", new JsonDeserializer() { - @Override - public ShimDataResponse deserialize(JsonParser jsonParser, DeserializationContext deserializationContext) - throws IOException { - - JsonNode responseNode = jsonParser.getCodec().readTree(jsonParser); - if (responseNode.path("WeightDataList").size() == 0) { - return ShimDataResponse.empty(IHealthShim.SHIM_KEY); - } - List datapoints = new ArrayList<>(); - for (JsonNode node : responseNode.path("WeightDataList")) { - datapoints.add(new BodyWeightBuilder().setTimeTaken(dateTimeValue(node.path("MDate"))) - .setWeight(node.path("WeightValue").asText(), "kg").build()); - } - Map results = new HashMap<>(); - results.put(BodyWeight.SCHEMA_BODY_WEIGHT, datapoints); - return ShimDataResponse.result(IHealthShim.SHIM_KEY, results); - } - }), - - SLEEP("sleep", new JsonDeserializer() { - @Override - public ShimDataResponse deserialize(JsonParser jsonParser, DeserializationContext deserializationContext) - throws IOException { - - JsonNode responseNode = jsonParser.getCodec().readTree(jsonParser); - if (responseNode.path("SRDataList").size() == 0) { - return ShimDataResponse.empty(IHealthShim.SHIM_KEY); - } - List sleepDurations = new ArrayList<>(); - for (JsonNode node : responseNode.path("SRDataList")) { - DateTime start = dateTimeValue(node.path("StartTime")); - DateTime end = dateTimeValue(node.path("EndTime")); - sleepDurations.add(new SleepDurationBuilder().withStartAndEnd(start, end) - .setNotes(textValue(node.path("Note"))).build()); - } - Map results = new HashMap<>(); - results.put(SleepDuration.SCHEMA_SLEEP_DURATION, sleepDurations); - return ShimDataResponse.result(IHealthShim.SHIM_KEY, results); - } - }), - - STEP_COUNT("activity", new JsonDeserializer() { - @Override - public ShimDataResponse deserialize(JsonParser jsonParser, DeserializationContext deserializationContext) - throws IOException { - - JsonNode responseNode = jsonParser.getCodec().readTree(jsonParser); - if (responseNode.path("ARDataList").size() == 0) { - return ShimDataResponse.empty(IHealthShim.SHIM_KEY); - } - List stepCounts = new ArrayList<>(); - for (JsonNode node : responseNode.path("ARDataList")) { - if (node.path("Steps").intValue() > 0) { - DateTime start = dateTimeValue(node.path("MDate")); - stepCounts.add(new StepCountBuilder() - .withStartAndDuration(start, 1.0, DurationUnitValue.DurationUnit.d) - .setSteps(node.path("Steps").intValue()).build()); - } - } - Map results = new HashMap<>(); - results.put(StepCount.SCHEMA_STEP_COUNT, stepCounts); - return ShimDataResponse.result(IHealthShim.SHIM_KEY, results); - } - }); - - private String endPoint; - - private JsonDeserializer normalizer; - - IHealthDataTypes(String endPoint, JsonDeserializer normalizer) { - this.endPoint = endPoint; - this.normalizer = normalizer; - } - - @Override - public JsonDeserializer getNormalizer() { - return normalizer; - } - - public String getEndPoint() { - return endPoint; - } - - private static DateTime dateTimeValue(JsonNode node) { - Preconditions.checkArgument(node.isIntegralNumber()); - return new DateTime(node.longValue() * 1000, DateTimeZone.UTC); - } - - private static String textValue(JsonNode node) { - Preconditions.checkArgument(node.isTextual()); - return Strings.emptyToNull(node.textValue().trim()); - } - } - - @Override - protected ResponseEntity getData(OAuth2RestOperations restTemplate, - ShimDataRequest shimDataRequest) throws ShimException { - - final IHealthDataTypes dataType; - try { - dataType = IHealthDataTypes.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."); - } - - OAuth2AccessToken token = SerializationUtils.deserialize(shimDataRequest.getAccessParameters().getSerializedToken()); - String userId = Preconditions.checkNotNull((String) token.getAdditionalInformation().get("UserID")); - String urlRequest = API_URL + "/user/" + userId + "/" + dataType.getEndPoint() + ".json?"; - DateTime now = new DateTime(); - DateTime startDate = shimDataRequest.getStartDate() == null ? - now.minusDays(1) : shimDataRequest.getStartDate(); - DateTime endDate = shimDataRequest.getEndDate() == null ? - now.plusDays(1) : shimDataRequest.getEndDate(); - urlRequest += "&start_time=" + startDate.getMillis() / 1000; - urlRequest += "&end_time=" + endDate.getMillis() / 1000; - urlRequest += "&page_index=1"; - urlRequest += "&client_id=" + restTemplate.getResource().getClientId(); - urlRequest += "&client_secret=" + restTemplate.getResource().getClientSecret(); - urlRequest += "&access_token=" + token.getValue(); - urlRequest += "&locale=default"; - - ResponseEntity responseEntity = restTemplate.getForEntity(urlRequest, byte[].class); - - ObjectMapper objectMapper = new ObjectMapper(); - try { - if (shimDataRequest.getNormalize()) { - SimpleModule module = new SimpleModule(); - module.addDeserializer(ShimDataResponse.class, dataType.getNormalizer()); - objectMapper.registerModule(module); - return new ResponseEntity<>( - objectMapper.readValue(responseEntity.getBody(), ShimDataResponse.class), HttpStatus.OK); - } else { - return new ResponseEntity<>( - ShimDataResponse.result(IHealthShim.SHIM_KEY, objectMapper.readTree(responseEntity.getBody())), HttpStatus.OK); - } - } catch (IOException e) { - e.printStackTrace(); - throw new ShimException("Could not read response data."); - } - } - - @Override - public OAuth2ProtectedResourceDetails getResource() { - AuthorizationCodeResourceDetails resource = (AuthorizationCodeResourceDetails) super.getResource(); - resource.setAuthenticationScheme(AuthenticationScheme.none); - return resource; - } - - @Override - protected String getAuthorizationUrl(UserRedirectRequiredException exception) { - final OAuth2ProtectedResourceDetails resource = getResource(); - return exception.getRedirectUri() - + "?client_id=" + resource.getClientId() - + "&response_type=code" - + "&APIName=" + Joiner.on(' ').join(resource.getScope()) - + "&redirect_uri=" + getCallbackUrl() + "?state=" + exception.getStateKey(); - } - - public class IHealthAuthorizationCodeAccessTokenProvider extends AuthorizationCodeAccessTokenProvider { - - public IHealthAuthorizationCodeAccessTokenProvider() { - this.setTokenRequestEnhancer(new RequestEnhancer() { - - @Override - public void enhance(AccessTokenRequest request, - OAuth2ProtectedResourceDetails resource, - MultiValueMap form, HttpHeaders headers) { - - form.set("client_id", resource.getClientId()); - form.set("client_secret", resource.getClientSecret()); - form.set("redirect_uri", getCallbackUrl()); - form.set("state", request.getStateKey()); - } - }); - } - - @Override - protected HttpMethod getHttpMethod() { - return HttpMethod.GET; - } - - @Override - protected ResponseExtractor getResponseExtractor() { - return new ResponseExtractor() { - - @Override - public OAuth2AccessToken extractData(ClientHttpResponse response) throws IOException { - - JsonNode node = new ObjectMapper().readTree(response.getBody()); - String token = Preconditions.checkNotNull(node.path("AccessToken").textValue(), "Missing access token: %s", node); - String refreshToken = Preconditions.checkNotNull(node.path("RefreshToken").textValue(), "Missing refresh token: %s" + node); - String userId = Preconditions.checkNotNull(node.path("UserID").textValue(), "Missing UserID: %s", node); - long expiresIn = node.path("Expires").longValue() * 1000; - Preconditions.checkArgument(expiresIn > 0, "Missing Expires: %s", node); - - DefaultOAuth2AccessToken accessToken = new DefaultOAuth2AccessToken(token); - accessToken.setExpiration(new Date(System.currentTimeMillis() + expiresIn)); - accessToken.setRefreshToken(new DefaultOAuth2RefreshToken(refreshToken)); - accessToken.setAdditionalInformation(ImmutableMap.of("UserID", userId)); - return accessToken; - } - }; - } - } -} diff --git a/shim-server/src/main/java/org/openmhealth/shim/ihealth/domain/IHealthTemporalRelationshipToMeal.java b/shim-server/src/main/java/org/openmhealth/shim/ihealth/domain/IHealthTemporalRelationshipToMeal.java new file mode 100644 index 00000000..0e4bf169 --- /dev/null +++ b/shim-server/src/main/java/org/openmhealth/shim/ihealth/domain/IHealthTemporalRelationshipToMeal.java @@ -0,0 +1,42 @@ +package org.openmhealth.shim.ihealth.domain; + +import org.openmhealth.schema.domain.omh.TemporalRelationshipToMeal; + +import java.util.Optional; + + +/** + * An enumeration of iHealth response values representing the temporal relationship between a blood glucose measure and + * a meal. + * + * @author Emerson Farrugia + */ +public enum IHealthTemporalRelationshipToMeal { + + BEFORE_BREAKFAST(TemporalRelationshipToMeal.BEFORE_BREAKFAST), + AFTER_BREAKFAST(TemporalRelationshipToMeal.AFTER_BREAKFAST), + BEFORE_LUNCH(TemporalRelationshipToMeal.BEFORE_LUNCH), + AFTER_LUNCH(TemporalRelationshipToMeal.AFTER_LUNCH), + BEFORE_DINNER(TemporalRelationshipToMeal.BEFORE_DINNER), + AFTER_DINNER(TemporalRelationshipToMeal.AFTER_DINNER), + AT_MIDNIGHT(TemporalRelationshipToMeal.AFTER_DINNER); + + private TemporalRelationshipToMeal standardConstant; + + IHealthTemporalRelationshipToMeal(TemporalRelationshipToMeal standardConstant) { + this.standardConstant = standardConstant; + } + + /** + * @return the standard constant used to refer to this temporal relationship + */ + public TemporalRelationshipToMeal getStandardConstant() { + return standardConstant; + } + + + public static Optional findByResponseValue(String responseValue) { + + return Optional.of(IHealthTemporalRelationshipToMeal.valueOf(responseValue.toUpperCase())); + } +} diff --git a/shim-server/src/main/java/org/openmhealth/shim/ihealth/iHealth.md b/shim-server/src/main/java/org/openmhealth/shim/ihealth/iHealth.md new file mode 100644 index 00000000..d279d85e --- /dev/null +++ b/shim-server/src/main/java/org/openmhealth/shim/ihealth/iHealth.md @@ -0,0 +1,238 @@ +### This is currently a work in progress + +# api +api url: https://api.ihealthlabs.com:8443/openapiv2/user/{userid} + +version: v2 + +api reference: http://developer.ihealthlabs.com/dev_documentation_openapidoc.htm + +## authentication + +### OAuth 2.0 +- protocol: OAuth 2.0 +- reference: https://api.ihealthlabs.com:8443/OpenApiV2/OAuthv2/userauthorization/ +- flows: authorization code + - authorization URL: https://api.ihealthlabs.com:8443/OpenApiV2/OAuthv2/userauthorization/ + - access token: https://api.ihealthlabs.com:8443/OpenApiV2/OAuthv2/userauthorization/ +- supports refresh tokens: yes + - refresh token: https://api.ihealthlabs.com:8443/OpenApiV2/OAuthv2/userauthorization/ + - response_type=refresh_token +- redirect_uri: uri must match the uri specified as a redirect_uri for the API developer account of the client key/secret being used +- scope: iHealth has its own name for the scope parameter in their authorization process: APIName. They do not refer to as scope in their API, but nonetheless it works the same as scope. During the authorization request you use the query parameter, “APIName” with values of one or more of the following, separated by spaces: + - OpenApiSleep + - OpenApiBP + - OpenApiWeight + - OpenApiActivity + - OpenApiBG + - OpenApiSpO2 + - OpenApiFood + - OpenApiSport +- access token: Authorization: Bearer token + - token lifetime: 3672000 seconds + +# pagination +- supported: yes, responses will have a “NextPageUrl” property if there is more data to be retrieved that is the URI for the endpoint uri for the next page. The next page url is endcoded such that it is in what appears to be an unusable in its current form, so it would need to be decoded in some way. However, there is a “page_index” parameter that can be incremented to step through pages of size 50 and reach all of the data. page_index = 1, 2, 3, 4, etc. + +- The limit is set at 50 and there is no ability to control page size or change the limit of the number of responses. + +# rate limit +- 5000 requests per hour per user + +# query updated by date +- unsupported + +# query created by date +- unsupported + +# time zone and time representation +- Each datapoint contains a “timezone” property, which is a utc-offset in the form of a string (“-0800” or "0800") or integer (-8 or 8) +- Timestamps are represented as unix epoch seconds offset by the zone offset found in the “timezone” property, so the timestamps are, in essence, in local time +- Requests take unix epoch seconds and match on local time + +# endpoints + +## endpoint query parameters +- Required parameters: + - client_id: The ID for the client request + - client_secret: The key for the request + - access_token: The token for access + - sc: The serial number for the client application + - sv: A unique identifier for the client’s use of the specific endpoint (one for each endpoint per project) + +- Optional parameters + - start_time: the unix epoch second to start the search for datapoints to return, when it is empty, the data would start from one year ago. + - locale: Default (example in Appendix) Set the locale / units returned + - end_time: the Unix epoch second to constrain the end of the search when the Activity data ends, it must be later than start_time + +## get weight +- Endpoint: /weight.json/ +- Reference: http://developer.ihealthlabs.com/dev_documentation_RequestfordataofWeight.htm + +### description +The endpoint returns body weight-related measurements that include weight, BMI, bone weight, and others. + +### response +The response contains meta information in the body, such as page length, record count, etc, as well as an array property “WeightDataList,” which contains the data. Each item in the list contains a set of unique properties describing the weight-related measurements taken/entered during that session and when that session occurred: + +- WeightValue +- MDate: measurement date +- TimeZone: time zone that the measurement occurred within +- BMI +- Note +- DataSource: ( Manual | FromDevice ) +- DataID + +It appears that the value is zero when it is missing, so zero values are not actually zero values, they are non values. + +All data in the response are rendered using the same unit, though it can change per response. The unit is contained in a property “WeightUnit” and is an integer between 0 - 2 corresponding to the following enum: {kg : 0}, {lbs : 1}, {stone : 2}. + +### measures +body-weight: mapped +body-mass-index: mapped + +## get blood pressure +- endpoint: /bp.json/ +- reference: http://developer.ihealthlabs.com/dev_documentation_RequestfordataofBloodPressure.htm + +### description +Retrieves blood pressure measurements that are created and stored in iHealth. + +### response +The response contains meta information in the body, such as page length, record count, etc, as well as an array property “BPDataList,” which contains the data. Each item in the list contains a set of properties describing the blood-pressure measurements: + +- BPL: some sort of WHO blood pressure level/rating +- DataID +- DataSource:( Manual | FromDevice) +- HP: systolic blood pressure +- HR: heart rate +- IsArr: whether the user has an arrythemia +- LP: diastolic blood pressure +- MDate: measurement date +- Note +- TimeZone: utc offset for the zone where the measurement occurred + +All data in the response are rendered using the same unit, though it can change per response. The unit is contained in a property “BPUnit” and is an integer between 0 - 1 corresponding to the following enum: {mmHg : 0}, {KPa : 1} + +### measures mapped +omh:blood-pressure +omh:heart-rate + +## get blood glucose +- endpoint: /glucose.json/ +- reference: http://developer.ihealthlabs.com/dev_documentation_RequestfordataofBG.htm + +### description +Retrieves a list of blood glucose measurements from the iHealth API. + +### response +The response contains meta information in the body, such as page length, record count, etc, as well as an array property “BGDataList,” which contains the data. Each item in the list contains a set of properties describing the blood glucose measurement: + +- BG: glucose value +- DataID: the unique identity +- DinnerSituation - ( Before_Breakfast | After_breakfast | Before_lunch | After_lunch | +- Before_dinner | After_dinner | At_midnight ) +- DrugSituation - ( Before_taking_pills | After_taking_pills ) +- MDate - measurement date time +- Note: the note of this data +- DataSource: ( Manual | FromDevice ) +- TimeZone: Time zone of measurement location + +All data in the response are rendered using the same unit, though it can change per response. The unit is contained in a property “BGUnit” and is an integer between 0 - 1 corresponding to the following enum: {mg/dl : 0}, {mmol/l : 1} + +### measures +- blood-glucose: mapped + +## get oxygen saturation +- endpoint: /spo2.json/ +- reference: http://developer.ihealthlabs.com/dev_documentation_RequestfordataofBloodOxygen.htm + +### description +Retrieves blood oxygen saturation and heart rate information stored in the iHealth API. + +### response +The response contains meta information in the body, such as page length, record count, etc, as well as an array property “BODataList,” which contains the data. Each item in the list contains a set of properties describing the blood glucose measurement: + +- BO: blood oxygen % saturation +- DataID: the unique identity +- HR: Heart rate +- MDate - measurement date time +- Note: the note of this data +- DataSource: ( Manual | FromDevice ) +- TimeZone: Time zone of measurement location + +### measures +heart-rate: mapped +oxygen-saturation: not mapped, schema and schema-sdk support is in development + +## get sports activities +- endpoint: /sport.json/ +- reference: http://developer.ihealthlabs.com/dev_documentation_RequestfordataofSport.htm + +### description +Retrieves the physical activities an individual has engaged in from the iHealth API + +### request +A note on the sport activity request: the start and end + +### response +The response contains meta information in the body, such as page length, record count, etc, as well as an array property “SPORTDataList,” which contains the data. Each item in the list contains a set of properties describing a unique physical activity: + +- SportName: the name of the activity +- SportStartTime: start time of the activity in unix epoch seconds +- SportEndTime: end time of the activity in unix epoch seconds +- TimeZone: Time zone of measurement location +- DataID: the unique identity +- Calories: The total calories consumed +- LastChangeTime: Time of last change (UTC) +- DataSource: ( Manual | FromDevice ) + +### measures +- physical-activity: mapped + +## get activity +- endpoint: /activity.json/ +- reference: http://developer.ihealthlabs.com/dev_documentation_RequestfordataofActivityReport.htm + +### description +Retrieves daily summaries of activity information (steps, calories, etc) that comes from an iHealth activity tracker device. + +### response +The response contains meta information in the body, such as page length, record count, etc, as well as an array property “ARDataList,” which contains the data. Each item in the list contains a set of properties describing a daily summary of activities for a given day: + +Calories: The total number of calories burned including BMR and activity calories +Steps: The total number of steps counted by the device +MDate: the datetime that this entry was last updated with new information or the end of the day on the date that this entry represents (if the day has already completed) +TimeZone: Time zone of measurement location +DataID: the unique identity +LastChangeTime: Time of last change (UTC) +DataSource: ( Manual | FromDevice ) + +### measures +- step-count: mapped +- calories-burned: not mapped because activity calories are combined with BMR + +## get sleep +- endpoint: /sleep.json/ +- reference: http://developer.ihealthlabs.com/dev_documentation_RequestfordataofSleepReport.htm + +### description +Returns a list of sleep activities for the user, including sleep summary information. + +### response +The response contains meta information in the body, such as page length, record count, etc, as well as an array property “SRDataList,” which contains the data. Each item in the list contains a set of properties describing the sleep event: + +“Awaken” - Number of times awoken +“Fallsleep” - Time until asleep +“HoursSlept” - The length of the sleep in minutes +“SleepEfficiency” - Sleep efficiency - unspecified +“StartTime” - Start time of sleep (in unix epoch probably) +“EndTime” - End time of sleep (in unix epoch probably) +DataSource: ( Manual | FromDevice ) +“TimeZone” - Time zone of measurement location + +### measures +sleep-duration: mapped + +## future endpoint support +We hope to support oxygen-saturation from the spo2 endpoint in iHealth, however we are finalizing a schema and schema-sdk support to represent that data. diff --git a/shim-server/src/main/java/org/openmhealth/shim/ihealth/mapper/IHealthBloodGlucoseDataPointMapper.java b/shim-server/src/main/java/org/openmhealth/shim/ihealth/mapper/IHealthBloodGlucoseDataPointMapper.java new file mode 100644 index 00000000..834d0b34 --- /dev/null +++ b/shim-server/src/main/java/org/openmhealth/shim/ihealth/mapper/IHealthBloodGlucoseDataPointMapper.java @@ -0,0 +1,111 @@ +/* + * 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.ihealth.mapper; + +import com.fasterxml.jackson.databind.JsonNode; +import org.openmhealth.schema.domain.omh.*; +import org.openmhealth.shim.ihealth.domain.IHealthTemporalRelationshipToMeal; + +import java.util.Optional; + +import static com.google.common.base.Preconditions.checkNotNull; +import static org.openmhealth.schema.domain.omh.BloodGlucoseUnit.MILLIGRAMS_PER_DECILITER; +import static org.openmhealth.schema.domain.omh.BloodGlucoseUnit.MILLIMOLES_PER_LITER; +import static org.openmhealth.shim.common.mapper.JsonNodeMappingSupport.asOptionalString; +import static org.openmhealth.shim.common.mapper.JsonNodeMappingSupport.asRequiredDouble; + + +/** + * A mapper that translates responses from the iHealth /glucose.json endpoint into {@link BloodGlucose} + * measures. + * + * @author Chris Schaefbauer + * @see endpoint documentation + */ +public class IHealthBloodGlucoseDataPointMapper extends IHealthDataPointMapper { + + public static final int MG_PER_DL_MAGIC_NUMBER = 0; + public static final int MMOL_PER_L_MAGIC_NUMBER = 1; + + @Override + protected String getListNodeName() { + return "BGDataList"; + } + + @Override + protected Optional getMeasureUnitNodeName() { + return Optional.of("BGUnit"); + } + + + @Override + protected Optional> asDataPoint(JsonNode listEntryNode, Integer measureUnitMagicNumber) { + + checkNotNull(measureUnitMagicNumber); + + double bloodGlucoseValue = asRequiredDouble(listEntryNode, "BG"); + + if (bloodGlucoseValue == 0) { + return Optional.empty(); + } + + BloodGlucoseUnit bloodGlucoseUnit = getBloodGlucoseUnitFromMagicNumber(measureUnitMagicNumber); + + BloodGlucose.Builder bloodGlucoseBuilder = + new BloodGlucose.Builder(new TypedUnitValue<>(bloodGlucoseUnit, bloodGlucoseValue)); + + Optional relationshipToMeal = asOptionalString(listEntryNode, "DinnerSituation"); + + if (relationshipToMeal.isPresent()) { + + IHealthTemporalRelationshipToMeal temporalRelationshipToMeal = + IHealthTemporalRelationshipToMeal.findByResponseValue(relationshipToMeal.get()).get(); + + bloodGlucoseBuilder.setTemporalRelationshipToMeal(temporalRelationshipToMeal.getStandardConstant()); + } + + getEffectiveTimeFrameAsDateTime(listEntryNode).ifPresent(bloodGlucoseBuilder::setEffectiveTimeFrame); + getUserNoteIfExists(listEntryNode).ifPresent(bloodGlucoseBuilder::setUserNotes); + + BloodGlucose bloodGlucose = bloodGlucoseBuilder.build(); + + /* The "temporal_relationship_to_medication" property is not part of the Blood Glucose schema, so its name and + values may change or we may remove support for this property at any time. */ + asOptionalString(listEntryNode, "DrugSituation").ifPresent( + drugSituation -> bloodGlucose + .setAdditionalProperty("temporal_relationship_to_medication", drugSituation)); + + return Optional.of(new DataPoint<>(createDataPointHeader(listEntryNode, bloodGlucose), bloodGlucose)); + } + + /** + * @param measureUnitMagicNumber The number from the iHealth response representing the unit of measure. + * @return The corresponding OMH schema unit of measure for blood glucose. + */ + protected BloodGlucoseUnit getBloodGlucoseUnitFromMagicNumber(Integer measureUnitMagicNumber) { + + if (measureUnitMagicNumber.equals(MG_PER_DL_MAGIC_NUMBER)) { + return MILLIGRAMS_PER_DECILITER; + } + else if (measureUnitMagicNumber.equals(MMOL_PER_L_MAGIC_NUMBER)) { + return MILLIMOLES_PER_LITER; + } + else { + throw new UnsupportedOperationException(); + } + } +} diff --git a/shim-server/src/main/java/org/openmhealth/shim/ihealth/mapper/IHealthBloodOxygenEndpointHeartRateDataPointMapper.java b/shim-server/src/main/java/org/openmhealth/shim/ihealth/mapper/IHealthBloodOxygenEndpointHeartRateDataPointMapper.java new file mode 100644 index 00000000..83d38ad1 --- /dev/null +++ b/shim-server/src/main/java/org/openmhealth/shim/ihealth/mapper/IHealthBloodOxygenEndpointHeartRateDataPointMapper.java @@ -0,0 +1,37 @@ +/* + * 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.ihealth.mapper; + +import org.openmhealth.schema.domain.omh.HeartRate; + + +/** + * A mapper that translates responses from the iHealth /spo2.json endpoint into {@link HeartRate} + * measures. + * + * @author Emerson Farrugia + * @author Chris Schaefbauer + * @see endpoint + * documentation + */ +public class IHealthBloodOxygenEndpointHeartRateDataPointMapper extends IHealthHeartRateDataPointMapper { + + @Override + protected String getListNodeName() { + return "BODataList"; + } +} diff --git a/shim-server/src/main/java/org/openmhealth/shim/ihealth/mapper/IHealthBloodPressureDataPointMapper.java b/shim-server/src/main/java/org/openmhealth/shim/ihealth/mapper/IHealthBloodPressureDataPointMapper.java new file mode 100644 index 00000000..adea11b7 --- /dev/null +++ b/shim-server/src/main/java/org/openmhealth/shim/ihealth/mapper/IHealthBloodPressureDataPointMapper.java @@ -0,0 +1,95 @@ +/* + * 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.ihealth.mapper; + +import com.fasterxml.jackson.databind.JsonNode; +import org.openmhealth.schema.domain.omh.*; + +import java.util.Optional; + +import static com.google.common.base.Preconditions.checkNotNull; +import static org.openmhealth.schema.domain.omh.BloodPressureUnit.MM_OF_MERCURY; +import static org.openmhealth.shim.common.mapper.JsonNodeMappingSupport.asRequiredDouble; + + +/** + * A mapper that translates responses from the iHealth /bp.json endpoint into {@link BloodPressure} + * measures. + * + * @author Chris Schaefbauer + * @see endpoint + * documentation + */ +public class IHealthBloodPressureDataPointMapper extends IHealthDataPointMapper { + + // Reference for conversion: http://www.ncbi.nlm.nih.gov/pmc/articles/PMC1603212/ + static final double KPA_TO_MMHG_CONVERSION_RATE = 7.50; + + static final int MMHG_UNIT_MAGIC_NUMBER = 0; + static final int KPA_UNIT_MAGIC_NUMBER = 1; + + @Override + protected String getListNodeName() { + return "BPDataList"; + } + + @Override + protected Optional getMeasureUnitNodeName() { + return Optional.of("BPUnit"); + } + + @Override + protected Optional> asDataPoint(JsonNode listEntryNode, Integer measureUnitMagicNumber) { + + checkNotNull(measureUnitMagicNumber); + + double systolicValue = + getBloodPressureValueInMmHg(asRequiredDouble(listEntryNode, "HP"), measureUnitMagicNumber); + SystolicBloodPressure systolicBloodPressure = new SystolicBloodPressure(MM_OF_MERCURY, systolicValue); + + double diastolicValue = + getBloodPressureValueInMmHg(asRequiredDouble(listEntryNode, "LP"), measureUnitMagicNumber); + DiastolicBloodPressure diastolicBloodPressure = new DiastolicBloodPressure(MM_OF_MERCURY, diastolicValue); + + BloodPressure.Builder bloodPressureBuilder = + new BloodPressure.Builder(systolicBloodPressure, diastolicBloodPressure); + + getEffectiveTimeFrameAsDateTime(listEntryNode).ifPresent(bloodPressureBuilder::setEffectiveTimeFrame); + + getUserNoteIfExists(listEntryNode).ifPresent(bloodPressureBuilder::setUserNotes); + + BloodPressure bloodPressure = bloodPressureBuilder.build(); + return Optional.of(new DataPoint<>(createDataPointHeader(listEntryNode, bloodPressure), bloodPressure)); + } + + /** + * @param measureUnitMagicNumber The number from the iHealth response representing the unit of measure. + * @return The corresponding OMH schema unit of measure for blood pressure. + */ + protected double getBloodPressureValueInMmHg(double rawBpValue, Integer measureUnitMagicNumber) { + + if (measureUnitMagicNumber.equals(MMHG_UNIT_MAGIC_NUMBER)) { + return rawBpValue; + } + else if (measureUnitMagicNumber.equals(KPA_UNIT_MAGIC_NUMBER)) { + return rawBpValue * KPA_TO_MMHG_CONVERSION_RATE; + } + else { + throw new UnsupportedOperationException(); + } + } +} diff --git a/shim-server/src/main/java/org/openmhealth/shim/ihealth/mapper/IHealthBloodPressureEndpointHeartRateDataPointMapper.java b/shim-server/src/main/java/org/openmhealth/shim/ihealth/mapper/IHealthBloodPressureEndpointHeartRateDataPointMapper.java new file mode 100644 index 00000000..fe00dd7a --- /dev/null +++ b/shim-server/src/main/java/org/openmhealth/shim/ihealth/mapper/IHealthBloodPressureEndpointHeartRateDataPointMapper.java @@ -0,0 +1,36 @@ +/* + * 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.ihealth.mapper; + +import org.openmhealth.schema.domain.omh.HeartRate; + + +/** + * A mapper that translates responses from the iHealth /bp.json endpoint into {@link HeartRate} measures. + * + * @author Emerson Farrugia + * @author Chris Schaefbauer + * @see endpoint + * documentation + */ +public class IHealthBloodPressureEndpointHeartRateDataPointMapper extends IHealthHeartRateDataPointMapper { + + @Override + protected String getListNodeName() { + return "BPDataList"; + } +} diff --git a/shim-server/src/main/java/org/openmhealth/shim/ihealth/mapper/IHealthBodyMassIndexDataPointMapper.java b/shim-server/src/main/java/org/openmhealth/shim/ihealth/mapper/IHealthBodyMassIndexDataPointMapper.java new file mode 100644 index 00000000..28063664 --- /dev/null +++ b/shim-server/src/main/java/org/openmhealth/shim/ihealth/mapper/IHealthBodyMassIndexDataPointMapper.java @@ -0,0 +1,71 @@ +/* + * 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.ihealth.mapper; + +import com.fasterxml.jackson.databind.JsonNode; +import org.openmhealth.schema.domain.omh.BodyMassIndex; +import org.openmhealth.schema.domain.omh.DataPoint; +import org.openmhealth.schema.domain.omh.TypedUnitValue; + +import java.util.Optional; + +import static org.openmhealth.schema.domain.omh.BodyMassIndexUnit.KILOGRAMS_PER_SQUARE_METER; +import static org.openmhealth.shim.common.mapper.JsonNodeMappingSupport.asRequiredDouble; + + +/** + * A mapper that translates responses from the iHealth /weight.json endpoint into {@link BodyMassIndex} + * measures. + * + * @author Emerson Farrugia + * @author Chris Schaefbauer + * @see endpoint + * documentation + */ +public class IHealthBodyMassIndexDataPointMapper extends IHealthDataPointMapper { + + @Override + protected String getListNodeName() { + return "WeightDataList"; + } + + @Override + protected Optional getMeasureUnitNodeName() { + return Optional.of("WeightUnit"); + } + + @Override + protected Optional> asDataPoint(JsonNode listEntryNode, Integer measureUnitMagicNumber) { + + Double bmiValue = asRequiredDouble(listEntryNode, "BMI"); + + if (bmiValue == 0) { + return Optional.empty(); + } + + BodyMassIndex.Builder bodyMassIndexBuilder = + new BodyMassIndex.Builder(new TypedUnitValue<>(KILOGRAMS_PER_SQUARE_METER, bmiValue)); + + getEffectiveTimeFrameAsDateTime(listEntryNode).ifPresent(bodyMassIndexBuilder::setEffectiveTimeFrame); + + getUserNoteIfExists(listEntryNode).ifPresent(bodyMassIndexBuilder::setUserNotes); + + BodyMassIndex bodyMassIndex = bodyMassIndexBuilder.build(); + return Optional.of(new DataPoint<>(createDataPointHeader(listEntryNode, bodyMassIndex), bodyMassIndex)); + + } +} diff --git a/shim-server/src/main/java/org/openmhealth/shim/ihealth/mapper/IHealthBodyWeightDataPointMapper.java b/shim-server/src/main/java/org/openmhealth/shim/ihealth/mapper/IHealthBodyWeightDataPointMapper.java new file mode 100644 index 00000000..97c1e0ba --- /dev/null +++ b/shim-server/src/main/java/org/openmhealth/shim/ihealth/mapper/IHealthBodyWeightDataPointMapper.java @@ -0,0 +1,148 @@ +/* + * 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.ihealth.mapper; + +import com.fasterxml.jackson.databind.JsonNode; +import org.openmhealth.schema.domain.omh.*; + +import java.util.Optional; + +import static com.google.common.base.Preconditions.checkNotNull; +import static org.openmhealth.shim.common.mapper.JsonNodeMappingSupport.asRequiredDouble; + + +/** + * A mapper that translates responses from the iHealth /weight.json endpoint into {@link BodyWeight} + * measures. + * + * @author Chris Schaefbauer + * @author Emerson Farrugia + * @see endpoint + * documentation + */ +public class IHealthBodyWeightDataPointMapper extends IHealthDataPointMapper { + + // Reference for conversion: https://en.wikipedia.org/wiki/Stone_(unit) + private static final double STONE_TO_KG_FACTOR = 6.3503; + + @Override + protected String getListNodeName() { + return "WeightDataList"; + } + + @Override + protected Optional getMeasureUnitNodeName() { + return Optional.of("WeightUnit"); + } + + @Override + protected Optional> asDataPoint(JsonNode listEntryNode, Integer measureUnitMagicNumber) { + + checkNotNull(measureUnitMagicNumber); + + IHealthBodyWeightUnit bodyWeightUnitType = IHealthBodyWeightUnit.fromIntegerValue(measureUnitMagicNumber); + MassUnit bodyWeightUnit = bodyWeightUnitType.getOmhUnit(); + + double bodyWeightValue = getBodyWeightValueForUnitType(listEntryNode, bodyWeightUnitType); + + if (bodyWeightValue == 0) { + + return Optional.empty(); + } + + BodyWeight.Builder bodyWeightBuilder = + new BodyWeight.Builder(new MassUnitValue(bodyWeightUnit, bodyWeightValue)); + + getEffectiveTimeFrameAsDateTime(listEntryNode).ifPresent(bodyWeightBuilder::setEffectiveTimeFrame); + + getUserNoteIfExists(listEntryNode).ifPresent(bodyWeightBuilder::setUserNotes); + + BodyWeight bodyWeight = bodyWeightBuilder.build(); + + return Optional.of(new DataPoint<>(createDataPointHeader(listEntryNode, bodyWeight), bodyWeight)); + } + + /** + * @param listEntryNode A single entry from the response result array. + * @param bodyWeightUnitType The unit type for the measure. + * @return The body weight value for the list entry that is rendered in the correct unit. + */ + protected double getBodyWeightValueForUnitType(JsonNode listEntryNode, + IHealthBodyWeightUnit bodyWeightUnitType) { + + Double weightValueFromApi = asRequiredDouble(listEntryNode, "WeightValue"); + return getBodyWeightValueForUnitType(weightValueFromApi, bodyWeightUnitType); + } + + /** + * @param bodyWeightValue The body weight value that has been extracted from the list entry node. + * @param bodyWeightUnitType The unit type for the measure. + * @return A body weight value that is rendered in the correct unit. + */ + protected double getBodyWeightValueForUnitType(double bodyWeightValue, IHealthBodyWeightUnit bodyWeightUnitType) { + + // iHealth has one unit type that is unsupported by OMH schemas, so we need to convert the value into a unit + // system that is supported by the schemas. + return bodyWeightValue * bodyWeightUnitType.getConversionFactorToOmh(); + } + + enum IHealthBodyWeightUnit { + + /* + The conversion factor handles conversions from unsupported mass units (currently the 'Stone' unit) to omh + supported units such that: + + ValueIntoSchema = ValueFromApi * Conversion + + We map STONE into KG because it is the SI unit for mass and the most widely accepted for measuring human + body weight in a clinical/scientific context. + */ + KG(0, MassUnit.KILOGRAM, 1), + LB(1, MassUnit.POUND, 1), + STONE(2, MassUnit.KILOGRAM, STONE_TO_KG_FACTOR); + + private final MassUnit omhUnit; + private final double conversionFactorToOmh; + private int magicNumber; + + IHealthBodyWeightUnit(int magicNumber, MassUnit omhUnit, double conversionFactor) { + this.omhUnit = omhUnit; + this.conversionFactorToOmh = conversionFactor; + this.magicNumber = magicNumber; + } + + public MassUnit getOmhUnit() { + return omhUnit; + } + + public double getConversionFactorToOmh() { + return conversionFactorToOmh; + } + + public static IHealthBodyWeightUnit fromIntegerValue(int unitValueFromApi) { + + for (IHealthBodyWeightUnit type : values()) { + if (type.magicNumber == unitValueFromApi) { + return type; + } + } + + throw new UnsupportedOperationException(); + } + + } +} diff --git a/shim-server/src/main/java/org/openmhealth/shim/ihealth/mapper/IHealthDataPointMapper.java b/shim-server/src/main/java/org/openmhealth/shim/ihealth/mapper/IHealthDataPointMapper.java new file mode 100644 index 00000000..559f38b8 --- /dev/null +++ b/shim-server/src/main/java/org/openmhealth/shim/ihealth/mapper/IHealthDataPointMapper.java @@ -0,0 +1,245 @@ +/* + * 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.ihealth.mapper; + +import com.fasterxml.jackson.databind.JsonNode; +import com.google.common.collect.Lists; +import org.openmhealth.schema.domain.omh.*; +import org.openmhealth.shim.common.mapper.DataPointMapper; + +import java.time.Instant; +import java.time.OffsetDateTime; +import java.time.ZoneId; +import java.time.ZoneOffset; +import java.util.List; +import java.util.Optional; +import java.util.UUID; + +import static com.google.common.base.Preconditions.checkNotNull; +import static java.time.Instant.ofEpochSecond; +import static java.time.OffsetDateTime.ofInstant; +import static org.openmhealth.schema.domain.omh.DataPointModality.SELF_REPORTED; +import static org.openmhealth.schema.domain.omh.DataPointModality.SENSED; +import static org.openmhealth.shim.common.mapper.JsonNodeMappingSupport.*; + + +/** + * The base class for mappers that translate iHealth API responses to {@link DataPoint} objects. + * + * @author Chris Schaefbauer + * @author Emerson Farrugia + */ +public abstract class IHealthDataPointMapper implements DataPointMapper { + + public static final String RESOURCE_API_SOURCE_NAME = "iHealth Resource API"; + public static final String DATA_SOURCE_MANUAL = "Manual"; + public static final String DATA_SOURCE_FROM_DEVICE = "FromDevice"; + + /** + * Maps a JSON response with individual data points contained in a JSON array to a list of {@link DataPoint} + * objects with the appropriate measure. Splits individual nodes and then iteratively maps the nodes in the list. + */ + @Override + public List> asDataPoints(List responseNodes) { + + // all mapped iHealth responses only require a single endpoint response + checkNotNull(responseNodes); + checkNotNull(responseNodes.size() == 1, "A single response node is allowed per call."); + + JsonNode responseNode = responseNodes.get(0); + + Integer measureUnitMagicNumber = null; + + if (getMeasureUnitNodeName().isPresent()) { + measureUnitMagicNumber = asRequiredInteger(responseNode, getMeasureUnitNodeName().get()); + } + + List> dataPoints = Lists.newArrayList(); + + for (JsonNode listEntryNode : asRequiredNode(responseNode, getListNodeName())) { + + asDataPoint(listEntryNode, measureUnitMagicNumber).ifPresent(dataPoints::add); + } + + return dataPoints; + } + + /** + * Creates a data point header with information describing the data point created around the measure. + *

+ * Note: Additional properties within the header come from the iHealth API and are not defined by the data point + * header schema. Additional properties are subject to change. + */ + protected DataPointHeader createDataPointHeader(JsonNode listEntryNode, Measure measure) { + + DataPointAcquisitionProvenance.Builder acquisitionProvenanceBuilder = + new DataPointAcquisitionProvenance.Builder(RESOURCE_API_SOURCE_NAME); + + asOptionalString(listEntryNode, "DataSource").ifPresent( + dataSource -> setAppropriateModality(dataSource, acquisitionProvenanceBuilder)); + + DataPointAcquisitionProvenance acquisitionProvenance = acquisitionProvenanceBuilder.build(); + + asOptionalString(listEntryNode, "DataID") + .ifPresent(externalId -> acquisitionProvenance.setAdditionalProperty("external_id", + externalId)); + + asOptionalLong(listEntryNode, "LastChangeTime").ifPresent( + lastUpdatedInUnixSecs -> acquisitionProvenance.setAdditionalProperty("source_updated_date_time", + ofInstant(ofEpochSecond(lastUpdatedInUnixSecs), ZoneId.of("Z")))); + + return new DataPointHeader.Builder(UUID.randomUUID().toString(), measure.getSchemaId()) + .setAcquisitionProvenance(acquisitionProvenance) + .build(); + + } + + /** + * Get an effective time frame based on the measurement date/time information in the list entry node. The effective + * time frame is set as a single point in time using an OffsetDateTime. This method does not get effective time + * frame as a time interval. + * + * @param listEntryNode A single node from the response result array. + */ + protected static Optional getEffectiveTimeFrameAsDateTime(JsonNode listEntryNode) { + + Optional weirdSeconds = asOptionalLong(listEntryNode, "MDate"); + + if (!weirdSeconds.isPresent()) { + return Optional.empty(); + } + + ZoneOffset zoneOffset = null; + + // if the time zone is a JSON string + if (asOptionalString(listEntryNode, "TimeZone").isPresent() && + !asOptionalString(listEntryNode, "TimeZone").get().isEmpty()) { + + zoneOffset = ZoneOffset.of(asOptionalString(listEntryNode, "TimeZone").get()); + } + // if the time zone is an JSON integer + else if (asOptionalLong(listEntryNode, "TimeZone").isPresent()) { + + Long timeZoneOffsetValue = asOptionalLong(listEntryNode, "TimeZone").get(); + + String timeZoneString = timeZoneOffsetValue.toString(); + + // Zone offset cannot parse a positive string offset that's missing a '+' sign (i.e., "0200" vs "+0200") + if (timeZoneOffsetValue >= 0) { + + timeZoneString = "+" + timeZoneString; + } + + zoneOffset = ZoneOffset.of(timeZoneString); + } + + if (zoneOffset == null) { + + return Optional.empty(); + } + + return Optional.of(new TimeFrame(getDateTimeWithCorrectOffset(weirdSeconds.get(), zoneOffset))); + } + + /** + * This method transforms a timestamp from an iHealth response (which is in the form of local time as epoch + * seconds) into an {@link OffsetDateTime} with the correct date/time and offset. The timestamps provided in + * iHealth responses are not unix epoch seconds in UTC but instead a unix epoch seconds value that is offset by the + * time zone of the data point. + */ + protected static OffsetDateTime getDateTimeWithCorrectOffset(Long localTimeAsEpochSeconds, ZoneOffset zoneOffset) { + + /* + iHealth provides the local time of a measurement as if it had occurred in UTC, along with the timezone + offset where the measurement occurred. To retrieve the correct OffsetDateTime, we must retain the local + date/time value, but replace the timezone offset. + */ + return OffsetDateTime.ofInstant(Instant.ofEpochSecond(localTimeAsEpochSeconds), ZoneOffset.UTC) + .withOffsetSameLocal(zoneOffset); + } + + /** + * @param dateTimeInUnixSecondsWithLocalTimeOffset A unix epoch timestamp in local time. + * @param timeZoneString The time zone offset as a String (e.g., "+0200","-2"). + * @return The date time with the correct offset. + */ + protected static OffsetDateTime getDateTimeAtStartOfDayWithCorrectOffset( + Long dateTimeInUnixSecondsWithLocalTimeOffset, String timeZoneString) { + + // Since the timestamps are in local time, we can use the local date time provided by rendering the timestamp + // in UTC, then translating that local time to the appropriate offset. + OffsetDateTime dateTimeFromOffsetInstant = + ofInstant(ofEpochSecond(dateTimeInUnixSecondsWithLocalTimeOffset), + ZoneId.of("Z")); + + return dateTimeFromOffsetInstant.toLocalDate().atStartOfDay().atOffset(ZoneOffset.of(timeZoneString)); + } + + /** + * Gets the user note from a list entry node if that property exists. + * + * @param listEntryNode A single entry from the response result array. + */ + protected static Optional getUserNoteIfExists(JsonNode listEntryNode) { + + Optional note = asOptionalString(listEntryNode, "Note"); + + if (note.isPresent() && !note.get().isEmpty()) { + + return note; + } + + return Optional.empty(); + } + + /** + * Sets the correct DataPointModality based on the iHealth value indicating the source of the DataPoint. + * + * @param dataSourceValue The iHealth value in the list entry node indicating the source of the DataPoint. + * @param builder The DataPointAcquisitionProvenance builder to set the modality. + */ + private void setAppropriateModality(String dataSourceValue, DataPointAcquisitionProvenance.Builder builder) { + + if (dataSourceValue.equals(DATA_SOURCE_FROM_DEVICE)) { + builder.setModality(SENSED); + } + else if (dataSourceValue.equals(DATA_SOURCE_MANUAL)) { + builder.setModality(SELF_REPORTED); + } + } + + /** + * @return The name of the JSON array that contains the individual data points. This is different per endpoint. + */ + protected abstract String getListNodeName(); + + /** + * @return The name of the JSON property whose value indicates the unit of measure used to render the values in the + * response. This is different per endpoint and some endpoints do not provide any units, in which case, the value + * should be an empty Optional. + */ + protected abstract Optional getMeasureUnitNodeName(); + + /** + * @param listEntryNode A single entry from the response result array. + * @param measureUnitMagicNumber The number representing the units used to render the response, according to + * iHealth. This is retrieved from the main body of the response node. If the measure type does not use units, then + * this value is null. + * @return The data point mapped from the listEntryNode, unless it is skipped. + */ + protected abstract Optional> asDataPoint(JsonNode listEntryNode, Integer measureUnitMagicNumber); +} diff --git a/shim-server/src/main/java/org/openmhealth/shim/ihealth/mapper/IHealthHeartRateDataPointMapper.java b/shim-server/src/main/java/org/openmhealth/shim/ihealth/mapper/IHealthHeartRateDataPointMapper.java new file mode 100644 index 00000000..0b31f73c --- /dev/null +++ b/shim-server/src/main/java/org/openmhealth/shim/ihealth/mapper/IHealthHeartRateDataPointMapper.java @@ -0,0 +1,59 @@ +/* + * 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.ihealth.mapper; + +import com.fasterxml.jackson.databind.JsonNode; +import org.openmhealth.schema.domain.omh.DataPoint; +import org.openmhealth.schema.domain.omh.HeartRate; + +import java.util.Optional; + +import static org.openmhealth.shim.common.mapper.JsonNodeMappingSupport.asRequiredDouble; + + +/** + * An abstract mapper that maps iHealth responses into {@link HeartRate} measures. + * + * @author Chris Schaefbauer + * @author Emerson Farrugia + */ +public abstract class IHealthHeartRateDataPointMapper extends IHealthDataPointMapper { + + @Override + protected Optional getMeasureUnitNodeName() { + return Optional.empty(); + } + + @Override + protected Optional> asDataPoint(JsonNode listEntryNode, Integer measureUnitMagicNumber) { + + double heartRateValue = asRequiredDouble(listEntryNode, "HR"); + + if (heartRateValue == 0) { + return Optional.empty(); + } + + HeartRate.Builder heartRateBuilder = new HeartRate.Builder(heartRateValue); + + getEffectiveTimeFrameAsDateTime(listEntryNode).ifPresent(heartRateBuilder::setEffectiveTimeFrame); + + getUserNoteIfExists(listEntryNode).ifPresent(heartRateBuilder::setUserNotes); + + HeartRate heartRate = heartRateBuilder.build(); + return Optional.of(new DataPoint<>(createDataPointHeader(listEntryNode, heartRate), heartRate)); + } +} diff --git a/shim-server/src/main/java/org/openmhealth/shim/ihealth/mapper/IHealthPhysicalActivityDataPointMapper.java b/shim-server/src/main/java/org/openmhealth/shim/ihealth/mapper/IHealthPhysicalActivityDataPointMapper.java new file mode 100644 index 00000000..f735a677 --- /dev/null +++ b/shim-server/src/main/java/org/openmhealth/shim/ihealth/mapper/IHealthPhysicalActivityDataPointMapper.java @@ -0,0 +1,88 @@ +/* + * 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.ihealth.mapper; + +import com.fasterxml.jackson.databind.JsonNode; +import org.openmhealth.schema.domain.omh.DataPoint; +import org.openmhealth.schema.domain.omh.PhysicalActivity; + +import java.time.ZoneOffset; +import java.util.Optional; + +import static org.openmhealth.schema.domain.omh.TimeInterval.ofStartDateTimeAndEndDateTime; +import static org.openmhealth.shim.common.mapper.JsonNodeMappingSupport.*; + + +/** + * A mapper that translates responses from the iHealth /sport.json endpoint into {@link PhysicalActivity} + * measures. + * + * @author Chris Schaefbauer + * @author Emerson Farrugia + * @see endpoint + * documentation + */ +public class IHealthPhysicalActivityDataPointMapper extends IHealthDataPointMapper { + + @Override + protected String getListNodeName() { + return "SPORTDataList"; + } + + @Override + protected Optional getMeasureUnitNodeName() { + + return Optional.empty(); + } + + @Override + protected Optional> asDataPoint(JsonNode listEntryNode, + Integer measureUnitMagicNumber) { + + String activityName = asRequiredString(listEntryNode, "SportName"); + + if (activityName.isEmpty()) { + + return Optional.empty(); + } + + PhysicalActivity.Builder physicalActivityBuilder = new PhysicalActivity.Builder(activityName); + + Optional startTimeUnixEpochSecs = asOptionalLong(listEntryNode, "SportStartTime"); + Optional endTimeUnixEpochSecs = asOptionalLong(listEntryNode, "SportEndTime"); + Optional timeZoneOffset = asOptionalInteger(listEntryNode, "TimeZone"); + + if (startTimeUnixEpochSecs.isPresent() && endTimeUnixEpochSecs.isPresent() && timeZoneOffset.isPresent()) { + + Integer timeZoneOffsetValue = timeZoneOffset.get(); + String timeZoneString = timeZoneOffsetValue.toString(); + + // Zone offset cannot parse a positive string offset that's missing a '+' sign (i.e., "0200" vs "+0200") + if (timeZoneOffsetValue >= 0) { + timeZoneString = "+" + timeZoneOffsetValue.toString(); + } + + physicalActivityBuilder.setEffectiveTimeFrame(ofStartDateTimeAndEndDateTime( + getDateTimeWithCorrectOffset(startTimeUnixEpochSecs.get(), ZoneOffset.of(timeZoneString)), + getDateTimeWithCorrectOffset(endTimeUnixEpochSecs.get(), ZoneOffset.of(timeZoneString)))); + } + + PhysicalActivity physicalActivity = physicalActivityBuilder.build(); + + return Optional.of(new DataPoint<>(createDataPointHeader(listEntryNode, physicalActivity), physicalActivity)); + } +} diff --git a/shim-server/src/main/java/org/openmhealth/shim/ihealth/mapper/IHealthSleepDurationDataPointMapper.java b/shim-server/src/main/java/org/openmhealth/shim/ihealth/mapper/IHealthSleepDurationDataPointMapper.java new file mode 100644 index 00000000..8259b37e --- /dev/null +++ b/shim-server/src/main/java/org/openmhealth/shim/ihealth/mapper/IHealthSleepDurationDataPointMapper.java @@ -0,0 +1,79 @@ +/* + * 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.ihealth.mapper; + +import com.fasterxml.jackson.databind.JsonNode; +import org.openmhealth.schema.domain.omh.*; + +import java.time.ZoneOffset; +import java.util.Optional; + +import static org.openmhealth.schema.domain.omh.TimeInterval.*; +import static org.openmhealth.shim.common.mapper.JsonNodeMappingSupport.*; + + +/** + * A mapper that translates responses from the iHealth /sleep.json endpoint into {@link SleepDuration} + * measures. + * + * @author Chris Schaefbauer + * @see endpoint + * documentation + */ +public class IHealthSleepDurationDataPointMapper extends IHealthDataPointMapper { + + @Override + protected String getListNodeName() { + return "SRDataList"; + } + + @Override + protected Optional getMeasureUnitNodeName() { + return Optional.empty(); + } + + @Override + protected Optional> asDataPoint(JsonNode listEntryNode, Integer measureUnitMagicNumber) { + + SleepDuration.Builder sleepDurationBuilder = new SleepDuration.Builder( + new DurationUnitValue(DurationUnit.MINUTE, asRequiredBigDecimal(listEntryNode, "HoursSlept"))); + + Optional startTime = asOptionalLong(listEntryNode, "StartTime"); + Optional endTime = asOptionalLong(listEntryNode, "EndTime"); + + if (startTime.isPresent() && endTime.isPresent()) { + + Optional timeZone = asOptionalString(listEntryNode, "TimeZone"); + + if (timeZone.isPresent()) { + + sleepDurationBuilder.setEffectiveTimeFrame(ofStartDateTimeAndEndDateTime( + getDateTimeWithCorrectOffset(startTime.get(), ZoneOffset.of(timeZone.get())), + getDateTimeWithCorrectOffset(endTime.get(), ZoneOffset.of(timeZone.get())))); + } + } + + getUserNoteIfExists(listEntryNode).ifPresent(sleepDurationBuilder::setUserNotes); + + SleepDuration sleepDuration = sleepDurationBuilder.build(); + + asOptionalBigDecimal(listEntryNode, "Awaken") + .ifPresent(awaken -> sleepDuration.setAdditionalProperty("wakeup_count", awaken)); + + return Optional.of(new DataPoint<>(createDataPointHeader(listEntryNode, sleepDuration), sleepDuration)); + } +} diff --git a/shim-server/src/main/java/org/openmhealth/shim/ihealth/mapper/IHealthStepCountDataPointMapper.java b/shim-server/src/main/java/org/openmhealth/shim/ihealth/mapper/IHealthStepCountDataPointMapper.java new file mode 100644 index 00000000..04a04f44 --- /dev/null +++ b/shim-server/src/main/java/org/openmhealth/shim/ihealth/mapper/IHealthStepCountDataPointMapper.java @@ -0,0 +1,87 @@ +/* + * 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.ihealth.mapper; + +import com.fasterxml.jackson.databind.JsonNode; +import org.openmhealth.schema.domain.omh.DataPoint; +import org.openmhealth.schema.domain.omh.DurationUnitValue; +import org.openmhealth.schema.domain.omh.StepCount; + +import java.math.BigDecimal; +import java.util.Optional; + +import static org.openmhealth.schema.domain.omh.DurationUnit.DAY; +import static org.openmhealth.schema.domain.omh.TimeInterval.ofStartDateTimeAndDuration; +import static org.openmhealth.shim.common.mapper.JsonNodeMappingSupport.*; + + +/** + * A mapper that translates responses from the iHealth /activity.json endpoint into {@link StepCount} + * measures. + * + * @author Chris Schaefbauer + * @see endpoint + * documentation + */ +public class IHealthStepCountDataPointMapper extends IHealthDataPointMapper { + + @Override + protected String getListNodeName() { + return "ARDataList"; + } + + @Override + protected Optional getMeasureUnitNodeName() { + return Optional.empty(); + } + + @Override + protected Optional> asDataPoint(JsonNode listEntryNode, Integer measureUnitMagicNumber) { + + BigDecimal steps = asRequiredBigDecimal(listEntryNode, "Steps"); + + if (steps.intValue() == 0) { + return Optional.empty(); + } + + StepCount.Builder stepCountBuilder = new StepCount.Builder(steps); + + Optional dateTimeString = asOptionalLong(listEntryNode, "MDate"); + + if (dateTimeString.isPresent()) { + + Optional timeZone = asOptionalString(listEntryNode, "TimeZone"); + + if (timeZone.isPresent()) { + + /* iHealth provides daily summaries for step counts and timestamp the datapoint at either the end of + the day (23:50) or at the latest time that datapoint was synced */ + stepCountBuilder.setEffectiveTimeFrame(ofStartDateTimeAndDuration( + getDateTimeAtStartOfDayWithCorrectOffset(dateTimeString.get(), timeZone.get()), + new DurationUnitValue(DAY, 1))); + } + } + + getUserNoteIfExists(listEntryNode).ifPresent(stepCountBuilder::setUserNotes); + + StepCount stepCount = stepCountBuilder.build(); + + return Optional.of(new DataPoint<>(createDataPointHeader(listEntryNode, stepCount), stepCount)); + } + + +} diff --git a/shim-server/src/main/resources/application.yaml b/shim-server/src/main/resources/application.yaml index 82b01332..70d56ee8 100644 --- a/shim-server/src/main/resources/application.yaml +++ b/shim-server/src/main/resources/application.yaml @@ -24,8 +24,21 @@ openmhealth: server: callbackUrlBase: http://localhost:8083 - #NOTE: Un-comment and fill in with your credentials if you're not using the UI + #NOTE: Un-comment and fill in the clientId/clientSecret with your credentials if you're not using the UI #NOTE: Un-comment and set partnerAccess to true if your credentials for a given API have partner access + #NOTE: Un-comment and fill in your serialValues for iHealth, otherwise the iHealth shim will not work correctly + #ihealth: + # serialValues: + # SC: [YOUR_SC_VALUE] + # sportSV: [YOUR_SV_VALUE_FOR_THE_SPORT_ENDPOINT] + # bloodPressureSV: [YOUR_SV_VALUE_FOR_THE_BLOOD_PRESSURE_ENDPOINT] + # spo2SV: [YOUR_SV_VALUE_FOR_THE_SPO2_ENDPOINT] + # weightSV: [YOUR_SV_VALUE_FOR_THE_WEIGHT_ENDPOINT] + # bloodGlucoseSV: [YOUR_SV_VALUE_FOR_THE_BLOOD_GLUCOSE_ENDPOINT] + # activitySV: [YOUR_SV_VALUE_FOR_THE_ACTIVITY_ENDPOINT] + # sleepSV: [YOUR_SV_VALUE_FOR_THE_SLEEP_ENDPOINT] + # clientId: [YOUR_CLIENT_ID] + # clientSecret: [YOUR_CLIENT_SECRET] #fitbit: # partnerAccess: true # clientId: [YOUR_CLIENT_ID] @@ -33,7 +46,6 @@ openmhealth: #fatsecret: # clientId: [YOUR_CLIENT_ID] # clientSecret: [YOUR_CLIENT_SECRET] - #ihealth: # clientId: [YOUR_CLIENT_ID] # clientSecret: [YOUR_CLIENT_SECRET] #jawbone: diff --git a/shim-server/src/test/java/org/openmhealth/shim/common/mapper/DataPointMapperUnitTests.java b/shim-server/src/test/java/org/openmhealth/shim/common/mapper/DataPointMapperUnitTests.java index 83c78005..171cf8c8 100644 --- a/shim-server/src/test/java/org/openmhealth/shim/common/mapper/DataPointMapperUnitTests.java +++ b/shim-server/src/test/java/org/openmhealth/shim/common/mapper/DataPointMapperUnitTests.java @@ -1,7 +1,13 @@ package org.openmhealth.shim.common.mapper; +import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; +import org.springframework.core.io.ClassPathResource; +import java.io.IOException; +import java.io.InputStream; + +import static java.lang.String.format; import static org.openmhealth.schema.configuration.JacksonConfiguration.newObjectMapper; @@ -11,4 +17,25 @@ public abstract class DataPointMapperUnitTests { protected static final ObjectMapper objectMapper = newObjectMapper(); + + + /** + * @param classPathResourceName the name of the class path resource to load + * @return the contents of the resource as a {@link JsonNode} + * @throws RuntimeException if the resource can't be loaded + */ + protected JsonNode asJsonNode(String classPathResourceName) { + + ClassPathResource resource = new ClassPathResource(classPathResourceName); + + try { + InputStream resourceInputStream = resource.getInputStream(); + return objectMapper.readTree(resourceInputStream); + } + catch (IOException e) { + throw new RuntimeException( + format("The class path resource '%s' can't be loaded as a JSON node.", classPathResourceName), e); + } + } + } diff --git a/shim-server/src/test/java/org/openmhealth/shim/ihealth/mapper/IHealthBloodGlucoseDataPointMapperUnitTests.java b/shim-server/src/test/java/org/openmhealth/shim/ihealth/mapper/IHealthBloodGlucoseDataPointMapperUnitTests.java new file mode 100644 index 00000000..ecaf346a --- /dev/null +++ b/shim-server/src/test/java/org/openmhealth/shim/ihealth/mapper/IHealthBloodGlucoseDataPointMapperUnitTests.java @@ -0,0 +1,126 @@ +/* + * 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.ihealth.mapper; + +import com.fasterxml.jackson.databind.JsonNode; +import org.openmhealth.schema.domain.omh.*; +import org.springframework.core.io.ClassPathResource; +import org.testng.annotations.BeforeMethod; +import org.testng.annotations.BeforeTest; +import org.testng.annotations.Test; + +import java.io.IOException; +import java.time.OffsetDateTime; +import java.util.List; + +import static java.util.Collections.singletonList; +import static org.hamcrest.CoreMatchers.equalTo; +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.empty; +import static org.openmhealth.schema.domain.omh.BloodGlucose.SCHEMA_ID; +import static org.openmhealth.schema.domain.omh.BloodGlucoseUnit.*; +import static org.openmhealth.schema.domain.omh.DataPointModality.SELF_REPORTED; +import static org.openmhealth.schema.domain.omh.DataPointModality.SENSED; +import static org.openmhealth.schema.domain.omh.TemporalRelationshipToMeal.*; + + +/** + * @author Chris Schaefbauer + */ +public class IHealthBloodGlucoseDataPointMapperUnitTests extends IHealthDataPointMapperUnitTests { + + private JsonNode responseNode; + private IHealthBloodGlucoseDataPointMapper mapper = new IHealthBloodGlucoseDataPointMapper(); + List> dataPoints; + + @BeforeTest + public void initializeResponseNode() throws IOException { + + responseNode = asJsonNode("/org/openmhealth/shim/ihealth/mapper/ihealth-blood-glucose.json"); + } + + @BeforeMethod + public void initializeDataPoints() { + + dataPoints = mapper.asDataPoints(singletonList(responseNode)); + } + + @Test + public void asDataPointsShouldReturnCorrectNumberOfDataPoints() { + + assertThat(dataPoints.size(), equalTo(2)); + } + + @Test + public void asDataPointsShouldReturnCorrectSensedDataPoints() { + + BloodGlucose.Builder expectedBloodGlucoseBuilder = new BloodGlucose.Builder( + new TypedUnitValue<>(MILLIGRAMS_PER_DECILITER, 60)) + .setEffectiveTimeFrame(OffsetDateTime.parse("2015-09-17T12:03:27-08:00")) + .setTemporalRelationshipToMeal(BEFORE_BREAKFAST) + .setUserNotes("Such glucose, much blood."); + + assertThat(dataPoints.get(0).getBody(), equalTo(expectedBloodGlucoseBuilder.build())); + + assertThat(dataPoints.get(0).getBody().getAdditionalProperty("temporal_relationship_to_medication").get(), + equalTo("Before_taking_pills")); + + testDataPointHeader(dataPoints.get(0).getHeader(), SCHEMA_ID, SENSED, + "f706b6152f684c0e9185b1fa6b7c5148", OffsetDateTime.parse("2015-09-17T20:03:41Z")); + } + + @Test + public void asDataPointsShouldReturnCorrectSelfReportedDataPoints() { + + BloodGlucose.Builder expectedBloodGlucoseBuilder = + new BloodGlucose.Builder(new TypedUnitValue<>(MILLIGRAMS_PER_DECILITER, 70)) + .setTemporalRelationshipToMeal(AFTER_BREAKFAST) + .setEffectiveTimeFrame(OffsetDateTime.parse("2015-09-24T14:44:40-06:00")); + + assertThat(dataPoints.get(1).getBody(), equalTo(expectedBloodGlucoseBuilder.build())); + + assertThat(dataPoints.get(1).getBody().getAdditionalProperty("temporal_relationship_to_medication").get(), + equalTo("After_taking_pills")); + + assertThat(dataPoints.get(1).getHeader().getAcquisitionProvenance().getModality(), equalTo(SELF_REPORTED)); + } + + @Test + public void asDataPointsShouldReturnNoDataPointsWhenBloodGlucoseListIsEmpty() throws IOException { + + ClassPathResource resource = + new ClassPathResource("/org/openmhealth/shim/ihealth/mapper/ihealth-empty-glucose.json"); + JsonNode emptyListResponseNode = objectMapper.readTree(resource.getInputStream()); + + assertThat(mapper.asDataPoints(singletonList(emptyListResponseNode)), is(empty())); + } + + @Test + public void getBloodGlucoseUnitFromMagicNumberShouldReturnCorrectBloodGlucoseUnit() { + + assertThat(mapper.getBloodGlucoseUnitFromMagicNumber(0), equalTo(MILLIGRAMS_PER_DECILITER)); + assertThat(mapper.getBloodGlucoseUnitFromMagicNumber(1), equalTo(MILLIMOLES_PER_LITER)); + } + + @Test(expectedExceptions = UnsupportedOperationException.class) + public void iHealthBloodGlucoseUnitEnumShouldThrowExceptionWhenInvalidMagicNumber() { + + mapper.getBloodGlucoseUnitFromMagicNumber(5); + } + +} diff --git a/shim-server/src/test/java/org/openmhealth/shim/ihealth/mapper/IHealthBloodOxygenEndpointHeartRateDataPointMapperUnitTests.java b/shim-server/src/test/java/org/openmhealth/shim/ihealth/mapper/IHealthBloodOxygenEndpointHeartRateDataPointMapperUnitTests.java new file mode 100644 index 00000000..b1a168ee --- /dev/null +++ b/shim-server/src/test/java/org/openmhealth/shim/ihealth/mapper/IHealthBloodOxygenEndpointHeartRateDataPointMapperUnitTests.java @@ -0,0 +1,117 @@ +/* + * 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.ihealth.mapper; + +import com.fasterxml.jackson.databind.JsonNode; +import org.openmhealth.schema.domain.omh.DataPoint; +import org.openmhealth.schema.domain.omh.HeartRate; +import org.testng.annotations.BeforeMethod; +import org.testng.annotations.BeforeTest; +import org.testng.annotations.Test; + +import java.io.IOException; +import java.time.OffsetDateTime; +import java.util.List; + +import static java.util.Collections.singletonList; +import static org.hamcrest.CoreMatchers.*; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.empty; +import static org.openmhealth.schema.domain.omh.DataPointModality.SELF_REPORTED; +import static org.openmhealth.schema.domain.omh.DataPointModality.SENSED; +import static org.openmhealth.schema.domain.omh.HeartRate.*; + + +/** + * @author Chris Schaefbauer + */ +public class IHealthBloodOxygenEndpointHeartRateDataPointMapperUnitTests extends IHealthDataPointMapperUnitTests { + + JsonNode responseNode; + + private IHealthBloodOxygenEndpointHeartRateDataPointMapper mapper = + new IHealthBloodOxygenEndpointHeartRateDataPointMapper(); + + List> dataPoints; + + @BeforeTest + public void initializeResponseNode() throws IOException { + + responseNode = asJsonNode("/org/openmhealth/shim/ihealth/mapper/ihealth-blood-oxygen.json"); + } + + @BeforeMethod + public void initializeDataPoints() { + + dataPoints = mapper.asDataPoints(singletonList(responseNode)); + } + + @Test + public void asDataPointsShouldReturnCorrectNumberOfDataPoints() { + + assertThat(dataPoints.size(), equalTo(2)); + } + + @Test + public void asDataPointsShouldReturnCorrectSensedDataPoints() { + + HeartRate.Builder expectedHeartRateBuilder = new HeartRate.Builder(80) + .setEffectiveTimeFrame(OffsetDateTime.parse("2015-09-23T15:46:00-06:00")); + + assertThat(dataPoints.get(0).getBody(), equalTo(expectedHeartRateBuilder.build())); + + testDataPointHeader(dataPoints.get(0).getHeader(), SCHEMA_ID, SENSED, + "d7fb9db14b0fc3e8e1635720c28bda64", OffsetDateTime.parse("2015-09-23T21:46:00Z")); + } + + @Test + public void asDataPointsShouldReturnCorrectSelfReportedDataPoints() { + + HeartRate.Builder expectedHeartRateBuilder = new HeartRate.Builder(65) + .setEffectiveTimeFrame(OffsetDateTime.parse("2015-09-24T15:03:00-06:00")) + .setUserNotes("Satch on satch "); + + assertThat(dataPoints.get(1).getBody(), equalTo(expectedHeartRateBuilder.build())); + + assertThat(dataPoints.get(1).getHeader().getAcquisitionProvenance().getModality(), equalTo(SELF_REPORTED)); + } + + @Test + public void asDataPointsShouldReturnCorrectUserNotesWithDataPoints() { + + assertThat(dataPoints.get(0).getBody().getUserNotes(), nullValue()); + assertThat(dataPoints.get(1).getBody().getUserNotes(), equalTo("Satch on satch ")); + } + + @Test + public void asDataPointsShouldReturnNoDataPointWhenHeartRateDataIsNotPresent() throws IOException { + + JsonNode noHeartRateBloodOxygenNode = asJsonNode( + "org/openmhealth/shim/ihealth/mapper/ihealth-blood-oxygen-missing-heart-rate.json"); + + assertThat(mapper.asDataPoints(singletonList(noHeartRateBloodOxygenNode)), is(empty())); + } + + @Test + public void asDataPointsShouldReturnEmptyListWhenEmptyIHealthResponse() { + + JsonNode emptyNode = asJsonNode("/org/openmhealth/shim/ihealth/mapper/ihealth-empty-blood-oxygen.json"); + + assertThat(mapper.asDataPoints(singletonList(emptyNode)), is(empty())); + + } +} diff --git a/shim-server/src/test/java/org/openmhealth/shim/ihealth/mapper/IHealthBloodPressureDataPointMapperUnitTests.java b/shim-server/src/test/java/org/openmhealth/shim/ihealth/mapper/IHealthBloodPressureDataPointMapperUnitTests.java new file mode 100644 index 00000000..9a06c4b2 --- /dev/null +++ b/shim-server/src/test/java/org/openmhealth/shim/ihealth/mapper/IHealthBloodPressureDataPointMapperUnitTests.java @@ -0,0 +1,135 @@ +/* + * 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.ihealth.mapper; + +import com.fasterxml.jackson.databind.JsonNode; +import org.openmhealth.schema.domain.omh.*; +import org.testng.annotations.BeforeMethod; +import org.testng.annotations.BeforeTest; +import org.testng.annotations.Test; + +import java.io.IOException; +import java.time.OffsetDateTime; +import java.util.List; + +import static java.util.Collections.singletonList; +import static org.hamcrest.CoreMatchers.*; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.empty; +import static org.openmhealth.schema.domain.omh.BloodPressure.*; +import static org.openmhealth.schema.domain.omh.BloodPressureUnit.*; +import static org.openmhealth.schema.domain.omh.DataPointModality.SELF_REPORTED; +import static org.openmhealth.schema.domain.omh.DataPointModality.SENSED; + + +/** + * @author Chris Schaefbauer + */ +public class IHealthBloodPressureDataPointMapperUnitTests extends IHealthDataPointMapperUnitTests { + + private JsonNode responseNode; + private IHealthBloodPressureDataPointMapper mapper = new IHealthBloodPressureDataPointMapper(); + List> dataPoints; + + @BeforeTest + public void initializeResponseNode() throws IOException { + + responseNode = asJsonNode("org/openmhealth/shim/ihealth/mapper/ihealth-blood-pressure.json"); + } + + @BeforeMethod + public void initializeDataPoints() { + + dataPoints = mapper.asDataPoints(singletonList(responseNode)); + } + + @Test + public void asDataPointsShouldReturnCorrectNumberOfDataPoints() { + + assertThat(dataPoints.size(), equalTo(2)); + } + + @Test + public void asDataPointsShouldReturnCorrectSensedDataPoints() { + + BloodPressure expectedBloodPressure = new BloodPressure.Builder( + new SystolicBloodPressure(MM_OF_MERCURY, 120), + new DiastolicBloodPressure(MM_OF_MERCURY, 90)) + .setEffectiveTimeFrame(OffsetDateTime.parse("2015-09-17T12:04:23-08:00")) + .build(); + + assertThat(dataPoints.get(0).getBody(), equalTo(expectedBloodPressure)); + + DataPointHeader testHeader = dataPoints.get(0).getHeader(); + + testDataPointHeader(testHeader, SCHEMA_ID, SENSED, "c62b84d9d4b7480a8ff2aef1465aa454", + OffsetDateTime.parse("2015-09-17T20:04:30Z")); + + } + + @Test + public void asDataPointsShouldReturnCorrectSelfReportedDataPoints() { + + BloodPressure expectedBloodPressure = new BloodPressure.Builder( + new SystolicBloodPressure(MM_OF_MERCURY, 130), + new DiastolicBloodPressure(MM_OF_MERCURY, 95)) + .setEffectiveTimeFrame(OffsetDateTime.parse("2015-09-17T14:07:45-06:00")) + .setUserNotes("BP on the up and up.") + .build(); + + assertThat(dataPoints.get(1).getBody(), equalTo(expectedBloodPressure)); + + DataPointHeader testHeader = dataPoints.get(1).getHeader(); + + assertThat(testHeader.getAcquisitionProvenance().getModality(), equalTo(SELF_REPORTED)); + + } + + @Test + public void asDataPointsShouldReturnCorrectUserNotesWithDataPoints() { + + assertThat(dataPoints.get(0).getBody().getUserNotes(), nullValue()); + assertThat(dataPoints.get(1).getBody().getUserNotes(), equalTo("BP on the up and up.")); + } + + @Test + public void asDataPointsShouldReturnEmptyListWhenEmptyIHealthResponse() { + + JsonNode emptyNode = asJsonNode("org/openmhealth/shim/ihealth/mapper/ihealth-empty-blood-pressure.json"); + + assertThat(mapper.asDataPoints(singletonList(emptyNode)), is(empty())); + } + + @Test + public void getBloodPressureValueInMmHgReturnsCorrectValueForMmHgUnit() { + + double bpValueFromMmHg = mapper.getBloodPressureValueInMmHg(120, 0); + assertThat(bpValueFromMmHg, equalTo(120.0)); + + double bpValueFromKpa = mapper.getBloodPressureValueInMmHg(16, 1); + assertThat(bpValueFromKpa, equalTo(120.0)); + + } + + @Test(expectedExceptions = UnsupportedOperationException.class) + public void getBloodPressureValueShouldThrowExceptionForInvalidEnum() { + + mapper.getBloodPressureValueInMmHg(12, 12); + } + + +} diff --git a/shim-server/src/test/java/org/openmhealth/shim/ihealth/mapper/IHealthBloodPressureEndpointHeartRateDataPointMapperUnitTests.java b/shim-server/src/test/java/org/openmhealth/shim/ihealth/mapper/IHealthBloodPressureEndpointHeartRateDataPointMapperUnitTests.java new file mode 100644 index 00000000..1ca5931d --- /dev/null +++ b/shim-server/src/test/java/org/openmhealth/shim/ihealth/mapper/IHealthBloodPressureEndpointHeartRateDataPointMapperUnitTests.java @@ -0,0 +1,110 @@ +/* + * 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.ihealth.mapper; + + +import com.fasterxml.jackson.databind.JsonNode; +import org.openmhealth.schema.domain.omh.DataPoint; +import org.openmhealth.schema.domain.omh.HeartRate; +import org.testng.annotations.BeforeMethod; +import org.testng.annotations.BeforeTest; +import org.testng.annotations.Test; + +import java.io.IOException; +import java.time.OffsetDateTime; +import java.util.List; + +import static java.util.Collections.singletonList; +import static org.hamcrest.CoreMatchers.*; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.collection.IsEmptyCollection.empty; +import static org.openmhealth.schema.domain.omh.DataPointModality.SELF_REPORTED; +import static org.openmhealth.schema.domain.omh.DataPointModality.SENSED; +import static org.openmhealth.schema.domain.omh.HeartRate.SCHEMA_ID; + + +/** + * @author Chris Schaefbauer + */ +public class IHealthBloodPressureEndpointHeartRateDataPointMapperUnitTests extends IHealthDataPointMapperUnitTests { + + JsonNode responseNode; + + private IHealthBloodPressureEndpointHeartRateDataPointMapper mapper = + new IHealthBloodPressureEndpointHeartRateDataPointMapper(); + + List> dataPoints; + + @BeforeTest + public void initializeResponseNodes() throws IOException { + + responseNode = asJsonNode("/org/openmhealth/shim/ihealth/mapper/ihealth-blood-pressure.json"); + } + + @BeforeMethod + public void initializeDataPoints() { + + dataPoints = mapper.asDataPoints(singletonList(responseNode)); + } + + + @Test + public void asDataPointsShouldReturnCorrectNumberOfDataPoints() { + + assertThat(dataPoints.size(), equalTo(2)); + } + + @Test + public void asDataPointsShouldReturnCorrectSensedDataPoints() { + + HeartRate.Builder expectedHeartRateBuilder = new HeartRate.Builder(100) + .setEffectiveTimeFrame(OffsetDateTime.parse("2015-09-17T12:04:23-08:00")); + HeartRate expectedSensedHeartRate = expectedHeartRateBuilder.build(); + assertThat(dataPoints.get(0).getBody(), equalTo(expectedSensedHeartRate)); + + testDataPointHeader(dataPoints.get(0).getHeader(), SCHEMA_ID, SENSED, + "c62b84d9d4b7480a8ff2aef1465aa454", OffsetDateTime.parse("2015-09-17T20:04:30Z")); + } + + @Test + public void asDataPointsShouldReturnCorrectSelfReportedDataPoints() { + + HeartRate expectedHeartRate = new HeartRate.Builder(75) + .setEffectiveTimeFrame(OffsetDateTime.parse("2015-09-17T14:07:45-06:00")) + .setUserNotes("BP on the up and up.") + .build(); + + assertThat(dataPoints.get(1).getBody(), equalTo(expectedHeartRate)); + assertThat(dataPoints.get(1).getHeader().getAcquisitionProvenance().getModality(), equalTo(SELF_REPORTED)); + } + + @Test + public void asDataPointsShouldReturnCorrectUserNotesWithDataPoints() { + + assertThat(dataPoints.get(0).getBody().getUserNotes(), nullValue()); + assertThat(dataPoints.get(1).getBody().getUserNotes(), equalTo("BP on the up and up.")); + } + + @Test + public void asDataPointsShouldReturnNoDataPointWhenHeartRateDataIsNotPresent() throws IOException { + + JsonNode noHeartRateBloodPressureNode = asJsonNode( + "org/openmhealth/shim/ihealth/mapper/ihealth-blood-pressure-missing-heart-rate.json"); + + assertThat(mapper.asDataPoints(singletonList(noHeartRateBloodPressureNode)), is(empty())); + } +} diff --git a/shim-server/src/test/java/org/openmhealth/shim/ihealth/mapper/IHealthBodyMassIndexDataPointMapperUnitTests.java b/shim-server/src/test/java/org/openmhealth/shim/ihealth/mapper/IHealthBodyMassIndexDataPointMapperUnitTests.java new file mode 100644 index 00000000..43d38d6c --- /dev/null +++ b/shim-server/src/test/java/org/openmhealth/shim/ihealth/mapper/IHealthBodyMassIndexDataPointMapperUnitTests.java @@ -0,0 +1,113 @@ +/* + * 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.ihealth.mapper; + +import com.fasterxml.jackson.databind.JsonNode; +import org.openmhealth.schema.domain.omh.BodyMassIndex; +import org.openmhealth.schema.domain.omh.DataPoint; +import org.openmhealth.schema.domain.omh.TypedUnitValue; +import org.testng.annotations.BeforeMethod; +import org.testng.annotations.BeforeTest; +import org.testng.annotations.Test; + +import java.io.IOException; +import java.time.OffsetDateTime; +import java.util.List; + +import static java.util.Collections.singletonList; +import static org.hamcrest.CoreMatchers.equalTo; +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.empty; +import static org.openmhealth.schema.domain.omh.BodyMassIndex.SCHEMA_ID; +import static org.openmhealth.schema.domain.omh.BodyMassIndexUnit.KILOGRAMS_PER_SQUARE_METER; +import static org.openmhealth.schema.domain.omh.DataPointModality.SELF_REPORTED; +import static org.openmhealth.schema.domain.omh.DataPointModality.SENSED; + + +/** + * @author Chris Schaefbauer + */ +public class IHealthBodyMassIndexDataPointMapperUnitTests extends IHealthDataPointMapperUnitTests { + + private JsonNode responseNode; + private IHealthBodyMassIndexDataPointMapper mapper = new IHealthBodyMassIndexDataPointMapper(); + List> dataPoints; + + @BeforeTest + public void initializeResponse() throws IOException { + + responseNode = asJsonNode("org/openmhealth/shim/ihealth/mapper/ihealth-body-weight.json"); + } + + @BeforeMethod + public void initializeDataPoints() { + + dataPoints = mapper.asDataPoints(singletonList(responseNode)); + } + + @Test + public void asDataPointsShouldReturnCorrectNumberOfDataPoints() { + + assertThat(dataPoints.size(), equalTo(2)); + } + + @Test + public void asDataPointsShouldReturnCorrectSensedDataPoints() { + + BodyMassIndex.Builder expectedBodyMassIndexBuilder = new BodyMassIndex.Builder(new TypedUnitValue<>( + KILOGRAMS_PER_SQUARE_METER, 22.56052563257619)) + .setEffectiveTimeFrame(OffsetDateTime.parse("2015-09-17T12:04:09-08:00")); + + assertThat(dataPoints.get(0).getBody(), equalTo(expectedBodyMassIndexBuilder.build())); + + testDataPointHeader(dataPoints.get(0).getHeader(), SCHEMA_ID, SENSED, "5fe5893c418b48cd8da7954f8b6c2f36", + OffsetDateTime.parse("2015-09-17T20:04:17Z")); + } + + @Test + public void asDataPointsShouldReturnCorrectSelfReportedDataPoints() { + + BodyMassIndex.Builder expectedBodyMassIndexBuilder = new BodyMassIndex.Builder( + new TypedUnitValue<>(KILOGRAMS_PER_SQUARE_METER, 22.56052398681641)) + .setEffectiveTimeFrame(OffsetDateTime.parse("2015-09-17T14:07:57-06:00")) + .setUserNotes("Weight so good, look at me now"); + + assertThat(dataPoints.get(1).getBody(), equalTo(expectedBodyMassIndexBuilder.build())); + + testDataPointHeader(dataPoints.get(1).getHeader(), SCHEMA_ID, SELF_REPORTED, + "b702a3a5e998f2fca268df6daaa69871", OffsetDateTime.parse("2015-09-17T20:08:00Z")); + } + + @Test + public void asDataPointsShouldReturnNoDataPointsWhenBodyMassIndexValueIsZero() throws IOException { + + JsonNode zeroValueNode = + asJsonNode("org/openmhealth/shim/ihealth/mapper/ihealth-missing-body-weight-value.json"); + + assertThat(mapper.asDataPoints(singletonList(zeroValueNode)), is(empty())); + } + + @Test + public void asDataPointsShouldReturnNoDataPointsWhenWeightListIsEmpty() throws IOException { + + JsonNode emptyListNode = asJsonNode("org/openmhealth/shim/ihealth/mapper/ihealth-empty-body-weight.json"); + + assertThat(mapper.asDataPoints(singletonList(emptyListNode)), is(empty())); + } + +} diff --git a/shim-server/src/test/java/org/openmhealth/shim/ihealth/mapper/IHealthBodyWeightDataPointMapperUnitTests.java b/shim-server/src/test/java/org/openmhealth/shim/ihealth/mapper/IHealthBodyWeightDataPointMapperUnitTests.java new file mode 100644 index 00000000..94b29f79 --- /dev/null +++ b/shim-server/src/test/java/org/openmhealth/shim/ihealth/mapper/IHealthBodyWeightDataPointMapperUnitTests.java @@ -0,0 +1,150 @@ +/* + * 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.ihealth.mapper; + +import com.fasterxml.jackson.databind.JsonNode; +import org.openmhealth.schema.domain.omh.*; +import org.testng.annotations.BeforeMethod; +import org.testng.annotations.BeforeTest; +import org.testng.annotations.Test; + +import java.io.IOException; +import java.time.OffsetDateTime; +import java.util.List; + +import static java.util.Collections.singletonList; +import static org.hamcrest.CoreMatchers.*; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.collection.IsEmptyCollection.empty; +import static org.openmhealth.schema.domain.omh.BodyWeight.*; +import static org.openmhealth.schema.domain.omh.DataPointModality.SELF_REPORTED; +import static org.openmhealth.schema.domain.omh.DataPointModality.SENSED; +import static org.openmhealth.shim.ihealth.mapper.IHealthBodyWeightDataPointMapper.*; + + +/** + * @author Chris Schaefbauer + */ +public class IHealthBodyWeightDataPointMapperUnitTests extends IHealthDataPointMapperUnitTests { + + protected JsonNode responseNode; + IHealthBodyWeightDataPointMapper mapper = new IHealthBodyWeightDataPointMapper(); + List> dataPoints; + + @BeforeTest + public void initializeResponseNode() throws IOException { + + responseNode = asJsonNode("org/openmhealth/shim/ihealth/mapper/ihealth-body-weight.json"); + } + + @BeforeMethod + public void initializeDataPoints() { + + dataPoints = mapper.asDataPoints(singletonList(responseNode)); + } + + @Test + public void asDataPointsShouldReturnCorrectNumberOfDataPoints() { + + assertThat(dataPoints.size(), equalTo(2)); + } + + @Test + public void asDataPointsShouldReturnCorrectSensedDataPoints() { + + BodyWeight.Builder expectedBodyWeightBuilder = new BodyWeight.Builder( + new MassUnitValue(MassUnit.KILOGRAM, 77.5643875134944)) + .setEffectiveTimeFrame(OffsetDateTime.parse("2015-09-17T12:04:09-08:00")); + + assertThat(dataPoints.get(0).getBody(), equalTo(expectedBodyWeightBuilder.build())); + + + DataPointHeader dataPointHeader = dataPoints.get(0).getHeader(); + testDataPointHeader(dataPointHeader, SCHEMA_ID, SENSED, "5fe5893c418b48cd8da7954f8b6c2f36", + OffsetDateTime.parse("2015-09-17T20:04:17Z")); + } + + @Test + public void asDataPointsShouldReturnCorrectSelfReportedDataPoints() { + + BodyWeight.Builder expectedBodyWeightBuilder = + new BodyWeight.Builder(new MassUnitValue(MassUnit.KILOGRAM, 77.56438446044922)) + .setEffectiveTimeFrame(OffsetDateTime.parse("2015-09-17T14:07:57-06:00")) + .setUserNotes("Weight so good, look at me now"); + + assertThat(dataPoints.get(1).getBody(), equalTo(expectedBodyWeightBuilder.build())); + + testDataPointHeader(dataPoints.get(1).getHeader(), SCHEMA_ID, SELF_REPORTED, + "b702a3a5e998f2fca268df6daaa69871", OffsetDateTime.parse("2015-09-17T20:08:00Z")); + + } + + @Test + public void asDataPointsShouldReturnCorrectUserNotes() { + + assertThat(dataPoints.get(0).getBody().getUserNotes(), nullValue()); + assertThat(dataPoints.get(1).getBody().getUserNotes(), equalTo("Weight so good, look at me now")); + } + + @Test + public void asDataPointsShouldReturnNoDataPointsWhenWeightValueEqualsZero() throws IOException { + + JsonNode zeroValueNode = + asJsonNode("org/openmhealth/shim/ihealth/mapper/ihealth-missing-body-weight-value.json"); + + assertThat(mapper.asDataPoints(singletonList(zeroValueNode)), is(empty())); + } + + @Test + public void asDataPointsShouldReturnNoDataPointsWhenWeightListIsEmpty() throws IOException { + + JsonNode emptyListNode = asJsonNode("org/openmhealth/shim/ihealth/mapper/ihealth-empty-body-weight.json"); + + assertThat(mapper.asDataPoints(singletonList(emptyListNode)), is(empty())); + } + + @Test + public void getBodyWeightValueForUnitTypeShouldReturnCorrectValueForOmhCompatibleTypes() { + + double bodyWeightValueForUnitType = mapper.getBodyWeightValueForUnitType(66.3, + IHealthBodyWeightUnit.KG); + assertThat(bodyWeightValueForUnitType, equalTo(66.3)); + + bodyWeightValueForUnitType = mapper.getBodyWeightValueForUnitType(100.5, + IHealthBodyWeightUnit.LB); + assertThat(bodyWeightValueForUnitType, equalTo(100.5)); + } + + @Test + public void getBodyWeightValueForUnitTypeShouldReturnCorrectValueForOmhIncompatibleTypes() { + + double bodyWeightValueForUnitType = mapper.getBodyWeightValueForUnitType(12.4, + IHealthBodyWeightUnit.STONE); + assertThat(bodyWeightValueForUnitType, equalTo(78.74372)); + + } + + @Test + public void getOmhUnitInBodyWeightUnitTypeShouldReturnCorrectMassUnits() { + + assertThat(IHealthBodyWeightUnit.KG.getOmhUnit(), equalTo(MassUnit.KILOGRAM)); + assertThat(IHealthBodyWeightUnit.LB.getOmhUnit(), equalTo(MassUnit.POUND)); + assertThat(IHealthBodyWeightUnit.STONE.getOmhUnit(), equalTo(MassUnit.KILOGRAM)); + } + + +} diff --git a/shim-server/src/test/java/org/openmhealth/shim/ihealth/mapper/IHealthDataPointMapperUnitTests.java b/shim-server/src/test/java/org/openmhealth/shim/ihealth/mapper/IHealthDataPointMapperUnitTests.java new file mode 100644 index 00000000..d7efdf07 --- /dev/null +++ b/shim-server/src/test/java/org/openmhealth/shim/ihealth/mapper/IHealthDataPointMapperUnitTests.java @@ -0,0 +1,48 @@ +/* + * 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.ihealth.mapper; + +import org.openmhealth.schema.domain.omh.DataPointHeader; +import org.openmhealth.schema.domain.omh.DataPointModality; +import org.openmhealth.schema.domain.omh.SchemaId; +import org.openmhealth.shim.common.mapper.DataPointMapperUnitTests; + +import java.time.OffsetDateTime; + +import static org.hamcrest.CoreMatchers.equalTo; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.openmhealth.shim.ihealth.mapper.IHealthDataPointMapper.*; + + +/** + * @author Chris Schaefbauer + */ +public class IHealthDataPointMapperUnitTests extends DataPointMapperUnitTests { + + protected void testDataPointHeader(DataPointHeader testHeader, SchemaId schemaId, DataPointModality modality, + String externalId, OffsetDateTime updatedDateTime) { + + assertThat(testHeader.getBodySchemaId(), equalTo(schemaId)); + assertThat(testHeader.getAcquisitionProvenance().getModality(), equalTo(modality)); + assertThat(testHeader.getAcquisitionProvenance().getSourceName(), + equalTo(RESOURCE_API_SOURCE_NAME)); + assertThat(testHeader.getAcquisitionProvenance().getAdditionalProperties().get("external_id"), equalTo( + externalId)); + assertThat(testHeader.getAcquisitionProvenance().getAdditionalProperties().get("source_updated_date_time"), + equalTo(updatedDateTime)); + } +} diff --git a/shim-server/src/test/java/org/openmhealth/shim/ihealth/mapper/IHealthDatapointMapperDateTimeUnitTests.java b/shim-server/src/test/java/org/openmhealth/shim/ihealth/mapper/IHealthDatapointMapperDateTimeUnitTests.java new file mode 100644 index 00000000..a384bb8b --- /dev/null +++ b/shim-server/src/test/java/org/openmhealth/shim/ihealth/mapper/IHealthDatapointMapperDateTimeUnitTests.java @@ -0,0 +1,146 @@ +/* + * 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.ihealth.mapper; + +import com.fasterxml.jackson.databind.JsonNode; +import org.openmhealth.schema.domain.omh.HeartRate; +import org.openmhealth.schema.domain.omh.TimeFrame; +import org.testng.annotations.BeforeMethod; +import org.testng.annotations.Test; + +import java.io.IOException; +import java.time.OffsetDateTime; +import java.util.Optional; + +import static org.hamcrest.CoreMatchers.equalTo; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.core.Is.is; +import static org.openmhealth.shim.ihealth.mapper.IHealthDataPointMapper.getDateTimeAtStartOfDayWithCorrectOffset; +import static org.openmhealth.shim.ihealth.mapper.IHealthDataPointMapper.getEffectiveTimeFrameAsDateTime; + + +/** + * @author Chris Schaefbauer + */ +public class IHealthDatapointMapperDateTimeUnitTests extends IHealthDataPointMapperUnitTests { + + HeartRate.Builder builder; + + @BeforeMethod + public void initializeBuilder() { + + builder = new HeartRate.Builder(45); + } + + @Test + public void setEffectiveTimeFrameShouldNotAddTimeFrameWhenTimeZoneIsMissing() throws IOException { + + JsonNode timeInfoNode = createResponseNodeWithTimeZone(null); + getEffectiveTimeFrameAsDateTime(timeInfoNode); + + assertThat(getEffectiveTimeFrameAsDateTime(timeInfoNode).isPresent(), is(false)); + } + + @Test + public void setEffectiveTimeFrameShouldNotAddTimeFrameWhenTimeZoneIsEmpty() throws IOException { + + JsonNode timeInfoNode = createResponseNodeWithTimeZone("\"\""); + + assertThat(getEffectiveTimeFrameAsDateTime(timeInfoNode).isPresent(), is(false)); + } + + @Test + public void setEffectiveTimeFrameReturnsTimeFrameInUtcWhenTimeZoneEqualsZero() throws IOException { + + testTimeFrameWhenItShouldBeSetCorrectly("0", "2015-11-17T18:24:23Z"); + } + + @Test + public void setEffectiveTimeFrameShouldAddCorrectTimeFrameWhenTimeZoneIsPositiveInteger() throws IOException { + + testTimeFrameWhenItShouldBeSetCorrectly("1", "2015-11-17T18:24:23+01:00"); + } + + @Test + public void setEffectiveTimeFrameShouldAddCorrectTimeFrameWhenTimeZoneIsNegativeInteger() throws IOException { + + testTimeFrameWhenItShouldBeSetCorrectly("-8", "2015-11-17T18:24:23-08:00"); + } + + @Test + public void setEffectiveTimeFrameShouldAddCorrectTimeFrameWhenTimeZoneIsPositiveOffsetString() throws IOException { + + testTimeFrameWhenItShouldBeSetCorrectly("\"+0100\"", "2015-11-17T18:24:23+01:00"); + } + + + @Test + public void setEffectiveTimeFrameShouldAddCorrectTimeFrameWhenTimeZoneIsNegativeOffsetString() throws IOException { + + testTimeFrameWhenItShouldBeSetCorrectly("\"-0700\"", "2015-11-17T18:24:23-07:00"); + } + + @Test + public void setEffectiveTimeFrameShouldAddTimeInUtcWhenTimeZoneIsZeroOffsetString() throws IOException { + + testTimeFrameWhenItShouldBeSetCorrectly("\"+0000\"", "2015-11-17T18:24:23Z"); + } + + @Test + public void getDateTimeAtStartOfDayWithCorrectOffsetShouldReturnCorrectDateTimeWhenTimeIsAtStartOfDay() { + + long startOfDayEpochSecond = OffsetDateTime.parse("2015-11-12T00:00:00Z").toEpochSecond(); + + OffsetDateTime dateTimeAtStartOfDay = getDateTimeAtStartOfDayWithCorrectOffset(startOfDayEpochSecond, "-0100"); + + assertThat(dateTimeAtStartOfDay, equalTo(OffsetDateTime.parse("2015-11-12T00:00:00-01:00"))); + } + + @Test + public void getDateTimeAtStartOfDayWithCorrectOffsetShouldReturnCorrectDateTimeWhenTimeIsAtEndOfDay() { + + long startOfDayEpochSecond = OffsetDateTime.parse("2015-11-12T23:59:59Z").toEpochSecond(); + + OffsetDateTime dateTimeAtStartOfDay = + getDateTimeAtStartOfDayWithCorrectOffset(startOfDayEpochSecond, "+0100"); + + assertThat(dateTimeAtStartOfDay, equalTo(OffsetDateTime.parse("2015-11-12T00:00:00+01:00"))); + } + + public void testTimeFrameWhenItShouldBeSetCorrectly(String timezoneString, String expectedDateTime) + throws IOException { + + JsonNode timeInfoNode = createResponseNodeWithTimeZone(timezoneString); + Optional effectiveTimeFrameAsDateTime = getEffectiveTimeFrameAsDateTime(timeInfoNode); + + assertThat(effectiveTimeFrameAsDateTime.isPresent(), is(true)); + assertThat(effectiveTimeFrameAsDateTime.get().getDateTime(), equalTo(OffsetDateTime.parse(expectedDateTime))); + } + + public JsonNode createResponseNodeWithTimeZone(String timezoneString) throws IOException { + + if (timezoneString == null) { + return objectMapper.readTree("{\"MDate\": 1447784663,\n" + + " \"Steps\": 100}\n"); + } + else { + return objectMapper.readTree("{\"MDate\": 1447784663,\n" + + " \"Steps\": 100,\n" + + "\"TimeZone\": " + timezoneString + "}\n"); + } + } +} diff --git a/shim-server/src/test/java/org/openmhealth/shim/ihealth/mapper/IHealthPhysicalActivityDataPointMapperUnitTests.java b/shim-server/src/test/java/org/openmhealth/shim/ihealth/mapper/IHealthPhysicalActivityDataPointMapperUnitTests.java new file mode 100644 index 00000000..f221b101 --- /dev/null +++ b/shim-server/src/test/java/org/openmhealth/shim/ihealth/mapper/IHealthPhysicalActivityDataPointMapperUnitTests.java @@ -0,0 +1,107 @@ +/* + * 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.ihealth.mapper; + +import com.fasterxml.jackson.databind.JsonNode; +import org.openmhealth.schema.domain.omh.DataPoint; +import org.openmhealth.schema.domain.omh.PhysicalActivity; +import org.openmhealth.schema.domain.omh.TimeInterval; +import org.testng.annotations.BeforeMethod; +import org.testng.annotations.BeforeTest; +import org.testng.annotations.Test; + +import java.io.IOException; +import java.time.OffsetDateTime; +import java.util.List; + +import static java.util.Collections.singletonList; +import static org.hamcrest.CoreMatchers.equalTo; +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.empty; +import static org.openmhealth.schema.domain.omh.DataPointModality.SELF_REPORTED; +import static org.openmhealth.schema.domain.omh.DataPointModality.SENSED; +import static org.openmhealth.schema.domain.omh.PhysicalActivity.SCHEMA_ID; + + +/** + * @author Chris Schaefbauer + */ +public class IHealthPhysicalActivityDataPointMapperUnitTests extends IHealthDataPointMapperUnitTests { + + + private JsonNode responseNode; + private IHealthPhysicalActivityDataPointMapper mapper = new IHealthPhysicalActivityDataPointMapper(); + List> dataPoints; + + @BeforeTest + public void initializeResponseNode() throws IOException { + + responseNode = asJsonNode("/org/openmhealth/shim/ihealth/mapper/ihealth-sports-activity.json"); + } + + @BeforeMethod + public void initializeDataPoints() { + + dataPoints = mapper.asDataPoints(singletonList(responseNode)); + } + + @Test + public void asDataPointsShouldReturnTheCorrectNumberOfDataPoints() { + + assertThat(dataPoints.size(), equalTo(2)); + } + + @Test + public void asDataPointsShouldReturnCorrectSensedDataPoints() { + + PhysicalActivity.Builder expectedPhysicalActivityBuilder = + new PhysicalActivity.Builder("Swimming, breaststroke") + .setEffectiveTimeFrame(TimeInterval.ofStartDateTimeAndEndDateTime( + OffsetDateTime.parse("2015-09-17T20:02:28-08:00"), + OffsetDateTime.parse("2015-09-17T20:32:28-08:00"))); + + assertThat(dataPoints.get(0).getBody(), equalTo(expectedPhysicalActivityBuilder.build())); + + testDataPointHeader(dataPoints.get(0).getHeader(), SCHEMA_ID, SENSED, + "3f8770f51cc84957a57d20f4fee1f34b", OffsetDateTime.parse("2015-09-17T20:02:57Z")); + } + + @Test + public void asDataPointsShouldReturnCorrectSelfReportedDataPoints() { + + PhysicalActivity.Builder expectedPhysicalActivityBuilder = new PhysicalActivity.Builder("Running") + .setEffectiveTimeFrame( + TimeInterval.ofStartDateTimeAndEndDateTime( + OffsetDateTime.parse("2015-09-22T20:43:03+01:00"), + OffsetDateTime.parse("2015-09-22T21:13:03+01:00"))); + + assertThat(dataPoints.get(1).getBody(), equalTo(expectedPhysicalActivityBuilder.build())); + + assertThat(dataPoints.get(1).getHeader().getAcquisitionProvenance().getModality(), equalTo(SELF_REPORTED)); + } + + @Test + public void asDataPointsReturnsNoDataPointsForAnEmptyList() throws IOException { + + JsonNode emptyListResponseNode = + asJsonNode("/org/openmhealth/shim/ihealth/mapper/ihealth-empty-sports-activity.json"); + + assertThat(mapper.asDataPoints(singletonList(emptyListResponseNode)), is(empty())); + } + +} diff --git a/shim-server/src/test/java/org/openmhealth/shim/ihealth/mapper/IHealthSleepDurationDataPointMapperUnitTests.java b/shim-server/src/test/java/org/openmhealth/shim/ihealth/mapper/IHealthSleepDurationDataPointMapperUnitTests.java new file mode 100644 index 00000000..0cdd4cf8 --- /dev/null +++ b/shim-server/src/test/java/org/openmhealth/shim/ihealth/mapper/IHealthSleepDurationDataPointMapperUnitTests.java @@ -0,0 +1,128 @@ +/* + * 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.ihealth.mapper; + +import com.fasterxml.jackson.databind.JsonNode; +import org.openmhealth.schema.domain.omh.DataPoint; +import org.openmhealth.schema.domain.omh.DurationUnitValue; +import org.openmhealth.schema.domain.omh.SleepDuration; +import org.testng.annotations.BeforeClass; +import org.testng.annotations.BeforeMethod; +import org.testng.annotations.Test; + +import java.math.BigDecimal; +import java.time.OffsetDateTime; +import java.util.List; + +import static java.util.Collections.singletonList; +import static org.hamcrest.CoreMatchers.equalTo; +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.CoreMatchers.nullValue; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.empty; +import static org.openmhealth.schema.domain.omh.DataPointModality.SELF_REPORTED; +import static org.openmhealth.schema.domain.omh.DataPointModality.SENSED; +import static org.openmhealth.schema.domain.omh.DurationUnit.MINUTE; +import static org.openmhealth.schema.domain.omh.SleepDuration.*; +import static org.openmhealth.schema.domain.omh.TimeInterval.ofStartDateTimeAndEndDateTime; + + +/** + * @author Chris Schaefbauer + */ +public class IHealthSleepDurationDataPointMapperUnitTests extends IHealthDataPointMapperUnitTests { + + private JsonNode responseNode; + private IHealthSleepDurationDataPointMapper mapper = new IHealthSleepDurationDataPointMapper(); + private List> dataPoints; + + @BeforeClass + public void initializeResponse() { + + responseNode = asJsonNode("org/openmhealth/shim/ihealth/mapper/ihealth-sleep.json"); + } + + @BeforeMethod + public void initializeDataPoints() { + + dataPoints = mapper.asDataPoints(singletonList(responseNode)); + } + + @Test + public void asDataPointsShouldReturnCorrectNumberOfDataPoints() { + + assertThat(dataPoints.size(), equalTo(3)); + } + + @Test + public void asDataPointsShouldReturnCorrectDataPointsWhenSensed() { + + SleepDuration.Builder expectedSleepDurationBuilder = new SleepDuration.Builder(new DurationUnitValue( + MINUTE, 345)); + + expectedSleepDurationBuilder.setEffectiveTimeFrame(ofStartDateTimeAndEndDateTime( + OffsetDateTime.parse("2015-11-15T01:51:00-07:00"), + OffsetDateTime.parse("2015-11-15T09:16:00-07:00"))); + + assertThat(dataPoints.get(0).getBody(), equalTo(expectedSleepDurationBuilder.build())); + + testDataPointHeader(dataPoints.get(0).getHeader(), SCHEMA_ID, SENSED, + "7eb7292b90d710ae7b7f61b75f9425cf", OffsetDateTime.parse("2015-11-15T16:19:10Z")); + } + + @Test + public void asDataPointsShouldMapAwakenAsAdditionalProperty() { + + assertThat(((BigDecimal) dataPoints.get(0).getBody().getAdditionalProperties().get("wakeup_count")).intValue(), + equalTo(13)); + + assertThat(((BigDecimal) dataPoints.get(2).getBody().getAdditionalProperties().get("wakeup_count")).intValue(), + equalTo(0)); + } + + @Test + public void asDataPointsShouldReturnDataPointWithUserNoteWhenNoteIsPresent() { + + SleepDuration.Builder expectedSleepDurationBuilder = + new SleepDuration.Builder(new DurationUnitValue(MINUTE, 195)); + + expectedSleepDurationBuilder.setEffectiveTimeFrame(ofStartDateTimeAndEndDateTime( + OffsetDateTime.parse("2015-11-15T13:51:00+01:00"), + OffsetDateTime.parse("2015-11-15T17:16:00+01:00"))); + + expectedSleepDurationBuilder.setUserNotes("Best sleep ever"); + + assertThat(dataPoints.get(1).getBody(), equalTo(expectedSleepDurationBuilder.build())); + + assertThat(dataPoints.get(0).getBody().getUserNotes(), nullValue()); + assertThat(dataPoints.get(1).getBody().getUserNotes(), equalTo("Best sleep ever")); + } + + @Test + public void asDataPointsShouldReturnCorrectDataPointsWhenManuallyEntered() { + + assertThat(dataPoints.get(2).getHeader().getAcquisitionProvenance().getModality(), equalTo(SELF_REPORTED)); + } + + @Test + public void asDataPointsShouldReturnEmptyListWhenEmptyIHealthResponse() { + + JsonNode emptyNode = asJsonNode("/org/openmhealth/shim/ihealth/mapper/ihealth-empty-sleep.json"); + + assertThat(mapper.asDataPoints(singletonList(emptyNode)), is(empty())); + } +} diff --git a/shim-server/src/test/java/org/openmhealth/shim/ihealth/mapper/IHealthStepCountDataPointMapperUnitTests.java b/shim-server/src/test/java/org/openmhealth/shim/ihealth/mapper/IHealthStepCountDataPointMapperUnitTests.java new file mode 100644 index 00000000..69a35e97 --- /dev/null +++ b/shim-server/src/test/java/org/openmhealth/shim/ihealth/mapper/IHealthStepCountDataPointMapperUnitTests.java @@ -0,0 +1,120 @@ +/* + * 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.ihealth.mapper; + +import com.fasterxml.jackson.databind.JsonNode; +import org.hamcrest.Matchers; +import org.openmhealth.schema.domain.omh.*; +import org.testng.annotations.BeforeClass; +import org.testng.annotations.BeforeMethod; +import org.testng.annotations.Test; + +import java.time.OffsetDateTime; +import java.util.List; + +import static java.util.Collections.singletonList; +import static org.hamcrest.CoreMatchers.equalTo; +import static org.hamcrest.CoreMatchers.nullValue; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.empty; +import static org.hamcrest.core.Is.is; +import static org.openmhealth.schema.domain.omh.DataPointModality.*; +import static org.openmhealth.schema.domain.omh.DurationUnit.*; +import static org.openmhealth.schema.domain.omh.StepCount.*; + + +/** + * @author Chris Schaefbauer + */ +public class IHealthStepCountDataPointMapperUnitTests extends IHealthDataPointMapperUnitTests { + + private JsonNode responseNode; + private IHealthStepCountDataPointMapper mapper = new IHealthStepCountDataPointMapper(); + List> dataPoints; + + + @BeforeClass + public void initializeResponseNode() { + + responseNode = asJsonNode("org/openmhealth/shim/ihealth/mapper/ihealth-activity.json"); + } + + @BeforeMethod + public void initializeDataPoints() { + + dataPoints = mapper.asDataPoints(singletonList(responseNode)); + } + + @Test + public void asDataPointsShouldNotMapDataPointsWithZeroSteps() { + + JsonNode nodeWithNoSteps = asJsonNode("org/openmhealth/shim/ihealth/mapper/ihealth-activity-no-steps.json"); + + assertThat(mapper.asDataPoints(singletonList(nodeWithNoSteps)), is(empty())); + } + + @Test + public void asDataPointsShouldReturnCorrectNumberOfDataPoints() { + + assertThat(mapper.asDataPoints(singletonList(responseNode)).size(), equalTo(2)); + } + + @Test + public void asDataPointsShouldReturnCorrectDataPointsWhenSensed() { + + StepCount.Builder expectedStepCountBuilder = new StepCount.Builder(21); + + expectedStepCountBuilder.setEffectiveTimeFrame( + TimeInterval.ofStartDateTimeAndDuration(OffsetDateTime.parse("2015-11-16T00:00:00+05:00"), + new DurationUnitValue(DAY, 1))); + + assertThat(dataPoints.get(0).getBody(), equalTo(expectedStepCountBuilder.build())); + + testDataPointHeader(dataPoints.get(0).getHeader(), SCHEMA_ID, SENSED, + "ac67c4ccf64af669d92569af85d19f59", OffsetDateTime.parse("2015-11-17T19:23:21Z")); + } + + @Test + public void asDataPointsShouldReturnDataPointWithUserNoteWhenNoteIsPresent() { + + StepCount.Builder expectedStepCountBuilder = new StepCount.Builder(4398); + + expectedStepCountBuilder.setEffectiveTimeFrame( + TimeInterval.ofStartDateTimeAndDuration(OffsetDateTime.parse("2015-11-18T00:00:00Z"), + new DurationUnitValue(DAY, 1))).setUserNotes("Great steps"); + + assertThat(dataPoints.get(1).getBody(), Matchers.equalTo(expectedStepCountBuilder.build())); + + assertThat(dataPoints.get(0).getBody().getUserNotes(), nullValue()); + assertThat(dataPoints.get(1).getBody().getUserNotes(), equalTo("Great steps")); + } + + @Test + public void asDataPointsShouldReturnSensedDataPointWhenManuallyEntered() { + + assertThat(mapper.asDataPoints(singletonList(responseNode)).get(1).getHeader().getAcquisitionProvenance() + .getModality(), equalTo(SELF_REPORTED)); + } + + @Test + public void asDataPointsShouldReturnEmptyListWhenEmptyIHealthResponse() { + + JsonNode emptyNode = asJsonNode("/org/openmhealth/shim/ihealth/mapper/ihealth-empty-activity-response.json"); + + assertThat(mapper.asDataPoints(singletonList(emptyNode)), is(empty())); + } +} diff --git a/shim-server/src/test/resources/org/openmhealth/shim/ihealth/mapper/ihealth-activity-no-steps.json b/shim-server/src/test/resources/org/openmhealth/shim/ihealth/mapper/ihealth-activity-no-steps.json new file mode 100644 index 00000000..820e6a1b --- /dev/null +++ b/shim-server/src/test/resources/org/openmhealth/shim/ihealth/mapper/ihealth-activity-no-steps.json @@ -0,0 +1,24 @@ +{ + "ARDataList": [ + { + "Calories": 1963, + "DataID": "de97222d4069d922b2ee8965d491c2e4", + "DataSource": "Manual", + "DistanceTraveled": 0, + "LastChangeTime": 1447826586, + "Lat": 0, + "Lon": 0, + "MDate": 1447800900, + "Note": "", + "Steps": 0, + "TimeZone": "-0700" + } + ], + "CurrentRecordCount": 1, + "DistanceUnit": 0, + "NextPageUrl": "", + "PageLength": 50, + "PageNumber": 1, + "PrevPageUrl": "", + "RecordCount": 1 +} \ No newline at end of file diff --git a/shim-server/src/test/resources/org/openmhealth/shim/ihealth/mapper/ihealth-activity.json b/shim-server/src/test/resources/org/openmhealth/shim/ihealth/mapper/ihealth-activity.json new file mode 100644 index 00000000..dc056e80 --- /dev/null +++ b/shim-server/src/test/resources/org/openmhealth/shim/ihealth/mapper/ihealth-activity.json @@ -0,0 +1,37 @@ +{ + "ARDataList": [ + { + "Calories": 1571, + "DataID": "ac67c4ccf64af669d92569af85d19f59", + "DataSource": "FromDevice", + "DistanceTraveled": 0.01743, + "LastChangeTime": 1447788201, + "Lat": 0, + "Lon": 0, + "MDate": 1447698000, + "Note": "", + "Steps": 21, + "TimeZone": "+0500" + }, + { + "Calories": 713, + "DataID": "a1c7ec7c4d75cf1fdb15e0d8acc4f83c", + "DataSource": "Manual", + "DistanceTraveled": 0, + "LastChangeTime": 1447827935, + "Lat": 0, + "Lon": 0, + "MDate": 1447831200, + "Note": "Great steps", + "Steps": 4398, + "TimeZone": "+0000" + } + ], + "CurrentRecordCount": 2, + "DistanceUnit": 0, + "NextPageUrl": "", + "PageLength": 50, + "PageNumber": 1, + "PrevPageUrl": "", + "RecordCount": 2 +} \ No newline at end of file diff --git a/shim-server/src/test/resources/org/openmhealth/shim/ihealth/mapper/ihealth-blood-glucose.json b/shim-server/src/test/resources/org/openmhealth/shim/ihealth/mapper/ihealth-blood-glucose.json new file mode 100644 index 00000000..60ba0a06 --- /dev/null +++ b/shim-server/src/test/resources/org/openmhealth/shim/ihealth/mapper/ihealth-blood-glucose.json @@ -0,0 +1,37 @@ +{ + "BGDataList": [ + { + "BG": 60, + "DataID": "f706b6152f684c0e9185b1fa6b7c5148", + "DataSource": "FromDevice", + "DinnerSituation": "Before_breakfast", + "DrugSituation": "Before_taking_pills", + "LastChangeTime": 1442520221, + "Lat": -1, + "Lon": -1, + "MDate": 1442491407, + "Note": "Such glucose, much blood.", + "TimeZone": "-0800" + }, + { + "BG": 70, + "DataID": "b0d61f8df4214f938a8d05d0ac583baa", + "DataSource": "Manual", + "DinnerSituation": "After_breakfast", + "DrugSituation": "After_taking_pills", + "LastChangeTime": 1443127602, + "Lat": -1, + "Lon": -1, + "MDate": 1443105880, + "Note": "", + "TimeZone": "-0600" + } + ], + "BGUnit": 0, + "CurrentRecordCount": 3, + "NextPageUrl": "", + "PageLength": 50, + "PageNumber": 1, + "PrevPageUrl": "", + "RecordCount": 3 +} \ No newline at end of file diff --git a/shim-server/src/test/resources/org/openmhealth/shim/ihealth/mapper/ihealth-blood-oxygen-missing-heart-rate.json b/shim-server/src/test/resources/org/openmhealth/shim/ihealth/mapper/ihealth-blood-oxygen-missing-heart-rate.json new file mode 100644 index 00000000..a5cf36fa --- /dev/null +++ b/shim-server/src/test/resources/org/openmhealth/shim/ihealth/mapper/ihealth-blood-oxygen-missing-heart-rate.json @@ -0,0 +1,22 @@ +{ + "BODataList": [ + { + "BO": 99, + "DataID": "d7fb9db14b0fc3e8e1635720c28bda64", + "DataSource": "FromDevice", + "HR": 0, + "LastChangeTime": 1443044760, + "Lat": 0, + "Lon": 0, + "MDate": 1443023160, + "Note": "", + "TimeZone": "-0600" + } + ], + "CurrentRecordCount": 1, + "NextPageUrl": "", + "PageLength": 50, + "PageNumber": 1, + "PrevPageUrl": "", + "RecordCount": 1 +} \ No newline at end of file diff --git a/shim-server/src/test/resources/org/openmhealth/shim/ihealth/mapper/ihealth-blood-oxygen.json b/shim-server/src/test/resources/org/openmhealth/shim/ihealth/mapper/ihealth-blood-oxygen.json new file mode 100644 index 00000000..e35aef3f --- /dev/null +++ b/shim-server/src/test/resources/org/openmhealth/shim/ihealth/mapper/ihealth-blood-oxygen.json @@ -0,0 +1,34 @@ +{ + "BODataList": [ + { + "BO": 99, + "DataID": "d7fb9db14b0fc3e8e1635720c28bda64", + "DataSource": "FromDevice", + "HR": 80, + "LastChangeTime": 1443044760, + "Lat": 0, + "Lon": 0, + "MDate": 1443023160, + "Note": "", + "TimeZone": "-0600" + }, + { + "BO": 90, + "DataID": "217e56a104cf8462f4977fd8bf743ca5", + "DataSource": "Manual", + "HR": 65, + "LastChangeTime": 1443128580, + "Lat": 0, + "Lon": 0, + "MDate": 1443106980, + "Note": "Satch on satch ", + "TimeZone": "-0600" + } + ], + "CurrentRecordCount": 2, + "NextPageUrl": "", + "PageLength": 50, + "PageNumber": 1, + "PrevPageUrl": "", + "RecordCount": 2 +} \ No newline at end of file diff --git a/shim-server/src/test/resources/org/openmhealth/shim/ihealth/mapper/ihealth-blood-pressure-missing-heart-rate.json b/shim-server/src/test/resources/org/openmhealth/shim/ihealth/mapper/ihealth-blood-pressure-missing-heart-rate.json new file mode 100644 index 00000000..c22dd10c --- /dev/null +++ b/shim-server/src/test/resources/org/openmhealth/shim/ihealth/mapper/ihealth-blood-pressure-missing-heart-rate.json @@ -0,0 +1,26 @@ +{ + "BPDataList": [ + { + "BPL": 3, + "DataID": "d6c08fb0bde6dc50db5c5f305a76c455", + "DataSource": "Manual", + "HP": 130, + "HR": 0, + "IsArr": 0, + "LP": 95, + "LastChangeTime": 1442520465, + "Lat": 0, + "Lon": 0, + "MDate": 1442498865, + "Note": "BP on the up and up.", + "TimeZone": "-0600" + } + ], + "BPUnit": 0, + "CurrentRecordCount": 1, + "NextPageUrl": "", + "PageLength": 50, + "PageNumber": 1, + "PrevPageUrl": "", + "RecordCount": 1 +} \ No newline at end of file diff --git a/shim-server/src/test/resources/org/openmhealth/shim/ihealth/mapper/ihealth-blood-pressure.json b/shim-server/src/test/resources/org/openmhealth/shim/ihealth/mapper/ihealth-blood-pressure.json new file mode 100644 index 00000000..20fa5f72 --- /dev/null +++ b/shim-server/src/test/resources/org/openmhealth/shim/ihealth/mapper/ihealth-blood-pressure.json @@ -0,0 +1,41 @@ +{ + "BPDataList": [ + { + "BPL": 3, + "DataID": "c62b84d9d4b7480a8ff2aef1465aa454", + "DataSource": "FromDevice", + "HP": 120, + "HR": 100, + "IsArr": -1, + "LP": 90, + "LastChangeTime": 1442520270, + "Lat": -1, + "Lon": -1, + "MDate": 1442491463, + "Note": "", + "TimeZone": "-0800" + }, + { + "BPL": 3, + "DataID": "d6c08fb0bde6dc50db5c5f305a76c455", + "DataSource": "Manual", + "HP": 130, + "HR": 75, + "IsArr": 0, + "LP": 95, + "LastChangeTime": 1442520465, + "Lat": 0, + "Lon": 0, + "MDate": 1442498865, + "Note": "BP on the up and up.", + "TimeZone": "-0600" + } + ], + "BPUnit": 0, + "CurrentRecordCount": 5, + "NextPageUrl": "", + "PageLength": 50, + "PageNumber": 1, + "PrevPageUrl": "", + "RecordCount": 5 +} \ No newline at end of file diff --git a/shim-server/src/test/resources/org/openmhealth/shim/ihealth/mapper/ihealth-body-weight.json b/shim-server/src/test/resources/org/openmhealth/shim/ihealth/mapper/ihealth-body-weight.json new file mode 100644 index 00000000..452fbf9f --- /dev/null +++ b/shim-server/src/test/resources/org/openmhealth/shim/ihealth/mapper/ihealth-body-weight.json @@ -0,0 +1,41 @@ +{ + "CurrentRecordCount": 4, + "NextPageUrl": "", + "PageLength": 50, + "PageNumber": 1, + "PrevPageUrl": "", + "RecordCount": 2, + "WeightDataList": [ + { + "BMI": 22.56052563257619, + "BoneValue": 0, + "DCI": 2035.774872767189, + "DataID": "5fe5893c418b48cd8da7954f8b6c2f36", + "DataSource": "FromDevice", + "FatValue": 10, + "LastChangeTime": 1442520257, + "MDate": 1442491449, + "MuscaleValue": 0, + "Note": "", + "TimeZone": "-0800", + "WaterValue": 25, + "WeightValue": 77.5643875134944 + }, + { + "BMI": 22.56052398681641, + "BoneValue": 0, + "DCI": 0, + "DataID": "b702a3a5e998f2fca268df6daaa69871", + "DataSource": "Manual", + "FatValue": 0, + "LastChangeTime": 1442520480, + "MDate": 1442498877, + "MuscaleValue": 0, + "Note": "Weight so good, look at me now", + "TimeZone": "-0600", + "WaterValue": 0, + "WeightValue": 77.56438446044922 + } + ], + "WeightUnit": 0 +} \ No newline at end of file diff --git a/shim-server/src/test/resources/org/openmhealth/shim/ihealth/mapper/ihealth-empty-activity-response.json b/shim-server/src/test/resources/org/openmhealth/shim/ihealth/mapper/ihealth-empty-activity-response.json new file mode 100644 index 00000000..eabb67c7 --- /dev/null +++ b/shim-server/src/test/resources/org/openmhealth/shim/ihealth/mapper/ihealth-empty-activity-response.json @@ -0,0 +1,10 @@ +{ + "ARDataList": [], + "CurrentRecordCount": 0, + "DistanceUnit": 0, + "NextPageUrl": "", + "PageLength": 50, + "PageNumber": 1, + "PrevPageUrl": "", + "RecordCount": 0 +} \ No newline at end of file diff --git a/shim-server/src/test/resources/org/openmhealth/shim/ihealth/mapper/ihealth-empty-blood-oxygen.json b/shim-server/src/test/resources/org/openmhealth/shim/ihealth/mapper/ihealth-empty-blood-oxygen.json new file mode 100644 index 00000000..895db9cb --- /dev/null +++ b/shim-server/src/test/resources/org/openmhealth/shim/ihealth/mapper/ihealth-empty-blood-oxygen.json @@ -0,0 +1,9 @@ +{ + "BODataList": [], + "CurrentRecordCount": 0, + "NextPageUrl": "", + "PageLength": 50, + "PageNumber": 1, + "PrevPageUrl": "", + "RecordCount": 0 +} \ No newline at end of file diff --git a/shim-server/src/test/resources/org/openmhealth/shim/ihealth/mapper/ihealth-empty-blood-pressure.json b/shim-server/src/test/resources/org/openmhealth/shim/ihealth/mapper/ihealth-empty-blood-pressure.json new file mode 100644 index 00000000..138bb791 --- /dev/null +++ b/shim-server/src/test/resources/org/openmhealth/shim/ihealth/mapper/ihealth-empty-blood-pressure.json @@ -0,0 +1,10 @@ +{ + "BPDataList": [], + "BPUnit": 0, + "CurrentRecordCount": 0, + "NextPageUrl": "", + "PageLength": 50, + "PageNumber": 1, + "PrevPageUrl": "", + "RecordCount": 0 +} \ No newline at end of file diff --git a/shim-server/src/test/resources/org/openmhealth/shim/ihealth/mapper/ihealth-empty-body-weight.json b/shim-server/src/test/resources/org/openmhealth/shim/ihealth/mapper/ihealth-empty-body-weight.json new file mode 100644 index 00000000..765b8503 --- /dev/null +++ b/shim-server/src/test/resources/org/openmhealth/shim/ihealth/mapper/ihealth-empty-body-weight.json @@ -0,0 +1,10 @@ +{ + "CurrentRecordCount": 0, + "NextPageUrl": "", + "PageLength": 50, + "PageNumber": 1, + "PrevPageUrl": "", + "RecordCount": 0, + "WeightDataList": [], + "WeightUnit": 0 +} \ No newline at end of file diff --git a/shim-server/src/test/resources/org/openmhealth/shim/ihealth/mapper/ihealth-empty-glucose.json b/shim-server/src/test/resources/org/openmhealth/shim/ihealth/mapper/ihealth-empty-glucose.json new file mode 100644 index 00000000..1f97ef02 --- /dev/null +++ b/shim-server/src/test/resources/org/openmhealth/shim/ihealth/mapper/ihealth-empty-glucose.json @@ -0,0 +1,10 @@ +{ + "BGDataList": [], + "BGUnit": 0, + "CurrentRecordCount": 0, + "NextPageUrl": "", + "PageLength": 50, + "PageNumber": 1, + "PrevPageUrl": "", + "RecordCount": 0 +} \ No newline at end of file diff --git a/shim-server/src/test/resources/org/openmhealth/shim/ihealth/mapper/ihealth-empty-sleep.json b/shim-server/src/test/resources/org/openmhealth/shim/ihealth/mapper/ihealth-empty-sleep.json new file mode 100644 index 00000000..9b372102 --- /dev/null +++ b/shim-server/src/test/resources/org/openmhealth/shim/ihealth/mapper/ihealth-empty-sleep.json @@ -0,0 +1,9 @@ +{ + "CurrentRecordCount": 0, + "NextPageUrl": "", + "PageLength": 50, + "PageNumber": 1, + "PrevPageUrl": "", + "RecordCount": 0, + "SRDataList": [] +} \ No newline at end of file diff --git a/shim-server/src/test/resources/org/openmhealth/shim/ihealth/mapper/ihealth-empty-sports-activity.json b/shim-server/src/test/resources/org/openmhealth/shim/ihealth/mapper/ihealth-empty-sports-activity.json new file mode 100644 index 00000000..33b065b7 --- /dev/null +++ b/shim-server/src/test/resources/org/openmhealth/shim/ihealth/mapper/ihealth-empty-sports-activity.json @@ -0,0 +1,9 @@ +{ + "CurrentRecordCount": 0, + "NextPageUrl": "", + "PageLength": 50, + "PageNumber": 1, + "PrevPageUrl": "", + "RecordCount": 0, + "SPORTDataList": [] +} \ No newline at end of file diff --git a/shim-server/src/test/resources/org/openmhealth/shim/ihealth/mapper/ihealth-missing-body-weight-value.json b/shim-server/src/test/resources/org/openmhealth/shim/ihealth/mapper/ihealth-missing-body-weight-value.json new file mode 100644 index 00000000..89955bb3 --- /dev/null +++ b/shim-server/src/test/resources/org/openmhealth/shim/ihealth/mapper/ihealth-missing-body-weight-value.json @@ -0,0 +1,26 @@ +{ + "CurrentRecordCount": 4, + "NextPageUrl": "", + "PageLength": 50, + "PageNumber": 1, + "PrevPageUrl": "", + "RecordCount": 1, + "WeightDataList": [ + { + "BMI": 0, + "BoneValue": 0, + "DCI": 0, + "DataID": "b702a3a5e998f2fca268df6daaa69871", + "DataSource": "Manual", + "FatValue": 0, + "LastChangeTime": 1442520480, + "MDate": 1442498877, + "MuscaleValue": 0, + "Note": "Weight so good, look at me meow", + "TimeZone": "-0600", + "WaterValue": 0, + "WeightValue": 0 + } + ], + "WeightUnit": 0 +} \ No newline at end of file diff --git a/shim-server/src/test/resources/org/openmhealth/shim/ihealth/mapper/ihealth-sleep.json b/shim-server/src/test/resources/org/openmhealth/shim/ihealth/mapper/ihealth-sleep.json new file mode 100644 index 00000000..4a553fb7 --- /dev/null +++ b/shim-server/src/test/resources/org/openmhealth/shim/ihealth/mapper/ihealth-sleep.json @@ -0,0 +1,55 @@ +{ + "CurrentRecordCount": 3, + "NextPageUrl": "", + "PageLength": 50, + "PageNumber": 1, + "PrevPageUrl": "", + "RecordCount": 3, + "SRDataList": [ + { + "Awaken": 13, + "DataID": "7eb7292b90d710ae7b7f61b75f9425cf", + "DataSource": "FromDevice", + "EndTime": 1447578960, + "FallSleep": 0, + "HoursSlept": 345, + "LastChangeTime": 1447604350, + "Lat": 0, + "Lon": 0, + "Note": "", + "SleepEfficiency": 77, + "StartTime": 1447552260, + "TimeZone": "-0700" + }, + { + "Awaken": 2, + "DataID": "34aa1c5351f965dbb6a09f3c5b730e83", + "DataSource": "FromDevice", + "EndTime": 1447607760, + "FallSleep": 0, + "HoursSlept": 195, + "LastChangeTime": 1447689288, + "Lat": 0, + "Lon": 0, + "Note": "Best sleep ever", + "SleepEfficiency": 95, + "StartTime": 1447595460, + "TimeZone": "+0100" + }, + { + "Awaken": 0, + "DataID": "ed650c323ebd78e444430291675bd2fd", + "DataSource": "Manual", + "EndTime": 1447657920, + "FallSleep": 0, + "HoursSlept": 355, + "LastChangeTime": 1447689288, + "Lat": 0, + "Lon": 0, + "Note": "", + "SleepEfficiency": 87, + "StartTime": 1447633620, + "TimeZone": "-0700" + } + ] +} \ No newline at end of file diff --git a/shim-server/src/test/resources/org/openmhealth/shim/ihealth/mapper/ihealth-sports-activity.json b/shim-server/src/test/resources/org/openmhealth/shim/ihealth/mapper/ihealth-sports-activity.json new file mode 100644 index 00000000..2744bc8a --- /dev/null +++ b/shim-server/src/test/resources/org/openmhealth/shim/ihealth/mapper/ihealth-sports-activity.json @@ -0,0 +1,34 @@ +{ + "CurrentRecordCount": 2, + "NextPageUrl": "", + "PageLength": 50, + "PageNumber": 1, + "PrevPageUrl": "", + "RecordCount": 2, + "SPORTDataList": [ + { + "Calories": 221.5, + "DataID": "3f8770f51cc84957a57d20f4fee1f34b", + "DataSource": "FromDevice", + "LastChangeTime": 1442520177, + "Lat": -1, + "Lon": -1, + "SportEndTime": 1442521948, + "SportName": "Swimming, breaststroke", + "SportStartTime": 1442520148, + "TimeZone": -8 + }, + { + "Calories": 202.5, + "DataID": "4bd1fdab8bab4e15950b7a95c1fff7ea", + "DataSource": "Manual", + "LastChangeTime": 1443127405, + "Lat": -1, + "Lon": -1, + "SportEndTime": 1442956383, + "SportName": "Running", + "SportStartTime": 1442954583, + "TimeZone": 1 + } + ] +} \ No newline at end of file