diff --git a/README.md b/README.md
index 7679e23d..2b8b1938 100644
--- a/README.md
+++ b/README.md
@@ -296,18 +296,20 @@ The currently supported shims are:
| fitbit1 | [step_count](https://dev.fitbit.com/docs/activity/#get-activity-intraday-time-series) (intraday)2 | [omh:step-count:2.0](http://www.openmhealth.org/documentation/#/schema-docs/schema-library/schemas/omh_step-count) | [FitbitIntradayStepCountDataPointMapper](https://github.com/openmhealth/shimmer/blob/master/shim-server/src/main/java/org/openmhealth/shim/fitbit/mapper/FitbitIntradayStepCountDataPointMapper.java) |
| googlefit | [body_height](https://developers.google.com/fit/rest/v1/data-types) | [omh:body-height:1.0](http://www.openmhealth.org/documentation/#/schema-docs/schema-library/schemas/omh_body-height) | [GoogleFitBodyHeightDataPointMapper](https://github.com/openmhealth/shimmer/blob/master/shim-server/src/main/java/org/openmhealth/shim/googlefit/mapper/GoogleFitBodyHeightDataPointMapper.java) |
| googlefit | [body_weight](https://developers.google.com/fit/rest/v1/data-types) | [omh:body-weight:1.0](http://www.openmhealth.org/documentation/#/schema-docs/schema-library/schemas/omh_body-weight) | [GoogleFitBodyWeightDataPointMapper](https://github.com/openmhealth/shimmer/blob/master/shim-server/src/main/java/org/openmhealth/shim/googlefit/mapper/GoogleFitBodyWeightDataPointMapper.java) |
-| googlefit | [calories_burned](https://developers.google.com/fit/rest/v1/data-types) | [omh:calories-burned:1.0](http://www.openmhealth.org/documentation/#/schema-docs/schema-library/schemas/omh_calories-burned) | [GoogleFitCaloriesBurnedDataPointMapper](https://github.com/openmhealth/shimmer/blob/master/shim-server/src/main/java/org/openmhealth/shim/googlefit/mapper/GoogleFitCaloriesBurnedDataPointMapper.java) |
+| googlefit | [calories_burned](https://developers.google.com/fit/rest/v1/data-types) | [omh:calories-burned:2.0](http://www.openmhealth.org/documentation/#/schema-docs/schema-library/schemas/omh_calories-burned) | [GoogleFitCaloriesBurnedDataPointMapper](https://github.com/openmhealth/shimmer/blob/master/shim-server/src/main/java/org/openmhealth/shim/googlefit/mapper/GoogleFitCaloriesBurnedDataPointMapper.java) |
+| googlefit | [geoposition](https://developers.google.com/fit/rest/v1/data-types) | [omh:geoposition:1.0](http://www.openmhealth.org/documentation/#/schema-docs/schema-library/schemas/omh_geoposition) | [GoogleFitGeopositionDataPointMapper](https://github.com/openmhealth/shimmer/blob/master/shim-server/src/main/java/org/openmhealth/shim/googlefit/mapper/GoogleFitGeopositionDataPointMapper.java) |
| googlefit | [heart_rate](https://developers.google.com/fit/rest/v1/data-types) | [omh:heart-rate:1.1](http://www.openmhealth.org/documentation/#/schema-docs/schema-library/schemas/omh_heart-rate) | [GoogleFitHeartRateDataPointMapper](https://github.com/openmhealth/shimmer/blob/master/shim-server/src/main/java/org/openmhealth/shim/googlefit/mapper/GoogleFitHeartRateDataPointMapper.java) |
| googlefit | [physical_activity](https://developers.google.com/fit/rest/v1/data-types) | [omh:physical-activity:1.2](http://www.openmhealth.org/documentation/#/schema-docs/schema-library/schemas/omh_physical-activity) | [GoogleFitPhysicalActivityDataPointMapper](https://github.com/openmhealth/shimmer/blob/master/shim-server/src/main/java/org/openmhealth/shim/googlefit/mapper/GoogleFitPhysicalActivityDataPointMapper.java) |
-| googlefit | [step_count](https://developers.google.com/fit/rest/v1/data-types) | [omh:step-count:1.0](http://www.openmhealth.org/documentation/#/schema-docs/schema-library/schemas/omh_step-count) | [GoogleFitStepCountDataPointMapper](https://github.com/openmhealth/shimmer/blob/master/shim-server/src/main/java/org/openmhealth/shim/googlefit/mapper/GoogleFitStepCountDataPointMapper.java) |
+| googlefit | [speed](https://developers.google.com/fit/rest/v1/data-types) | [omh:speed:1.0](http://www.openmhealth.org/documentation/#/schema-docs/schema-library/schemas/omh_speed) | [GoogleFitSpeedDataPointMapper](https://github.com/openmhealth/shimmer/blob/master/shim-server/src/main/java/org/openmhealth/shim/googlefit/mapper/GoogleFitSpeedDataPointMapper.java) |
+| googlefit | [step_count](https://developers.google.com/fit/rest/v1/data-types) | [omh:step-count:2.0](http://www.openmhealth.org/documentation/#/schema-docs/schema-library/schemas/omh_step-count) | [GoogleFitStepCountDataPointMapper](https://github.com/openmhealth/shimmer/blob/master/shim-server/src/main/java/org/openmhealth/shim/googlefit/mapper/GoogleFitStepCountDataPointMapper.java) |
| ihealth | [blood_glucose](http://developer.ihealthlabs.com/dev_documentation_RequestfordataofBG.htm) | [omh:blood-glucose:1.0](http://www.openmhealth.org/documentation/#/schema-docs/schema-library/schemas/omh_blood-glucose) | [IHealthBloodGlucoseDataPointMapper](https://github.com/openmhealth/shimmer/blob/master/shim-server/src/main/java/org/openmhealth/shim/ihealth/mapper/IHealthBloodGlucoseDataPointMapper.java) |
| ihealth | [blood_pressure](http://developer.ihealthlabs.com/dev_documentation_RequestfordataofBloodPressure.htm) | [omh:blood-pressure:1.0](http://www.openmhealth.org/documentation/#/schema-docs/schema-library/schemas/omh_blood-pressure) | [IHealthBloodPressureDataPointMapper](https://github.com/openmhealth/shimmer/blob/master/shim-server/src/main/java/org/openmhealth/shim/ihealth/mapper/IHealthBloodPressureDataPointMapper.java) |
| ihealth | [body_mass_index](http://developer.ihealthlabs.com/dev_documentation_RequestfordataofWeight.htm) | [omh:body-mass-index:1.0](http://www.openmhealth.org/documentation/#/schema-docs/schema-library/schemas/omh_body-mass-index) | [IHealthBodyMassIndexDataPointMapper](https://github.com/openmhealth/shimmer/blob/master/shim-server/src/main/java/org/openmhealth/shim/ihealth/mapper/IHealthBodyMassIndexDataPointMapper.java) |
| ihealth | [body_weight](http://developer.ihealthlabs.com/dev_documentation_RequestfordataofWeight.htm) | [omh:body-weight:1.0](http://www.openmhealth.org/documentation/#/schema-docs/schema-library/schemas/omh_body-weight) | [IHealthBodyWeightDataPointMapper](https://github.com/openmhealth/shimmer/blob/master/shim-server/src/main/java/org/openmhealth/shim/ihealth/mapper/IHealthBodyWeightDataPointMapper.java) |
| ihealth | [heart_rate](http://developer.ihealthlabs.com/dev_documentation_RequestfordataofActivityReport.htm) | [omh:heart-rate:1.1](http://www.openmhealth.org/documentation/#/schema-docs/schema-library/schemas/omh_heart-rate) | [IHealthHeartRateDataPointMapper](https://github.com/openmhealth/shimmer/blob/master/shim-server/src/main/java/org/openmhealth/shim/ihealth/mapper/IHealthHeartRateDataPointMapper.java) |
| ihealth | [physical_activity](http://developer.ihealthlabs.com/dev_documentation_RequestfordataofSport.htm) | [omh:physical-activity:1.2](http://www.openmhealth.org/documentation/#/schema-docs/schema-library/schemas/omh_physical-activity) | [IHealthPhysicalActivityDataPointMapper](https://github.com/openmhealth/shimmer/blob/master/shim-server/src/main/java/org/openmhealth/shim/ihealth/mapper/IHealthPhysicalActivityDataPointMapper.java) |
-| ihealth | [sleep_duration](http://developer.ihealthlabs.com/dev_documentation_RequestfordataofSleepReport.htm) | [omh:sleep-duration:1.0](http://www.openmhealth.org/documentation/#/schema-docs/schema-library/schemas/omh_sleep-duration) | [IHealthSleepDurationDataPointMapper](https://github.com/openmhealth/shimmer/blob/master/shim-server/src/main/java/org/openmhealth/shim/ihealth/mapper/IHealthSleepDurationDataPointMapper.java) |
-| ihealth | [step_count](http://developer.ihealthlabs.com/dev_documentation_RequestfordataofActivityReport.htm) | [omh:step-count:1.0](http://www.openmhealth.org/documentation/#/schema-docs/schema-library/schemas/omh_step-count) | [IHealthStepCountDataPointMapper](https://github.com/openmhealth/shimmer/blob/master/shim-server/src/main/java/org/openmhealth/shim/ihealth/mapper/IHealthStepCountDataPointMapper.java) |
+| ihealth | [sleep_duration](http://developer.ihealthlabs.com/dev_documentation_RequestfordataofSleepReport.htm) | [omh:sleep-duration:2.0](http://www.openmhealth.org/documentation/#/schema-docs/schema-library/schemas/omh_sleep-duration) | [IHealthSleepDurationDataPointMapper](https://github.com/openmhealth/shimmer/blob/master/shim-server/src/main/java/org/openmhealth/shim/ihealth/mapper/IHealthSleepDurationDataPointMapper.java) |
+| ihealth | [step_count](http://developer.ihealthlabs.com/dev_documentation_RequestfordataofActivityReport.htm) | [omh:step-count:2.0](http://www.openmhealth.org/documentation/#/schema-docs/schema-library/schemas/omh_step-count) | [IHealthStepCountDataPointMapper](https://github.com/openmhealth/shimmer/blob/master/shim-server/src/main/java/org/openmhealth/shim/ihealth/mapper/IHealthStepCountDataPointMapper.java) |
| jawbone | [body_mass_index](https://jawbone.com/up/developer/endpoints/body) | [omh:body-mass-index:1.0](http://www.openmhealth.org/documentation/#/schema-docs/schema-library/schemas/omh_body-mass-index) | [JawboneBodyMassIndexDataPointMapper](https://github.com/openmhealth/shimmer/blob/master/shim-server/src/main/java/org/openmhealth/shim/jawbone/mapper/JawboneBodyMassIndexDataPointMapper.java) |
| jawbone | [body_weight](https://jawbone.com/up/developer/endpoints/body) | [omh:body-weight:1.0](http://www.openmhealth.org/documentation/#/schema-docs/schema-library/schemas/omh_body-weight) | [JawboneBodyWeightDataPointMapper](https://github.com/openmhealth/shimmer/blob/master/shim-server/src/main/java/org/openmhealth/shim/jawbone/mapper/JawboneBodyWeightDataPointMapper.java) |
| jawbone | [heart_rate](https://jawbone.com/up/developer/endpoints)3 | [omh:heart-rate:1.1](http://www.openmhealth.org/documentation/#/schema-docs/schema-library/schemas/omh_heart-rate) | [JawboneHeartRateDataPointMapper](https://github.com/openmhealth/shimmer/blob/master/shim-server/src/main/java/org/openmhealth/shim/jawbone/mapper/JawboneHeartRateDataPointMapper.java) |
@@ -315,21 +317,24 @@ The currently supported shims are:
| jawbone | [sleep_duration](https://jawbone.com/up/developer/endpoints/sleeps) | [omh:sleep-duration:1.0](http://www.openmhealth.org/documentation/#/schema-docs/schema-library/schemas/omh_sleep-duration) | [JawboneSleepDurationDataPointMapper](https://github.com/openmhealth/shimmer/blob/master/shim-server/src/main/java/org/openmhealth/shim/jawbone/mapper/JawboneSleepDurationDataPointMapper.java) |
| jawbone | [step_count](https://jawbone.com/up/developer/endpoints) | [omh:step-count:1.0](http://www.openmhealth.org/documentation/#/schema-docs/schema-library/schemas/omh_step-count) | [JawboneStepCountDataPointMapper](https://github.com/openmhealth/shimmer/blob/master/shim-server/src/main/java/org/openmhealth/shim/jawbone/mapper/JawboneStepCountDataPointMapper.java) |
| misfit | [physical_activity](https://build.misfit.com/docs/cloudapi/api_references#session) | [omh:physical-activity:1.2](http://www.openmhealth.org/documentation/#/schema-docs/schema-library/schemas/omh_physical-activity) | [MisfitPhysicalActivityDataPointMapper](https://github.com/openmhealth/shimmer/blob/master/shim-server/src/main/java/org/openmhealth/shim/misfit/mapper/MisfitPhysicalActivityDataPointMapper.java) |
-| misfit | [step_count](https://build.misfit.com/docs/cloudapi/api_references#steps) | [omh:step-count:1.0](http://www.openmhealth.org/documentation/#/schema-docs/schema-library/schemas/omh_step-count) | [MisfitStepCountDataPointMapper](https://github.com/openmhealth/shimmer/blob/master/shim-server/src/main/java/org/openmhealth/shim/misfit/mapper/MisfitStepCountDataPointMapper.java) |
-| misfit | [sleep_duration](https://build.misfit.com/docs/cloudapi/api_references#sleep) | [omh:sleep-duration:1.0](http://www.openmhealth.org/documentation/#/schema-docs/schema-library/schemas/omh_sleep-duration) | [MisfitSleepDurationDataPointMapper](https://github.com/openmhealth/shimmer/blob/master/shim-server/src/main/java/org/openmhealth/shim/misfit/mapper/MisfitSleepDurationDataPointMapper.java) |
+| misfit | [step_count](https://build.misfit.com/docs/cloudapi/api_references#steps) | [omh:step-count21.0](http://www.openmhealth.org/documentation/#/schema-docs/schema-library/schemas/omh_step-count) | [MisfitStepCountDataPointMapper](https://github.com/openmhealth/shimmer/blob/master/shim-server/src/main/java/org/openmhealth/shim/misfit/mapper/MisfitStepCountDataPointMapper.java) |
+| misfit | [sleep_duration](https://build.misfit.com/docs/cloudapi/api_references#sleep) | [omh:sleep-duration:2.0](http://www.openmhealth.org/documentation/#/schema-docs/schema-library/schemas/omh_sleep-duration) | [MisfitSleepDurationDataPointMapper](https://github.com/openmhealth/shimmer/blob/master/shim-server/src/main/java/org/openmhealth/shim/misfit/mapper/MisfitSleepDurationDataPointMapper.java) |
+| misfit | [sleep_episode](https://build.misfit.com/docs/cloudapi/api_references#sleep) | [omh:sleep-episode:1.0](http://www.openmhealth.org/documentation/#/schema-docs/schema-library/schemas/omh_sleep-episode) | [MisfitSleepEpisodeDataPointMapper](https://github.com/openmhealth/shimmer/blob/master/shim-server/src/main/java/org/openmhealth/shim/misfit/mapper/MisfitSleepEpisodeDataPointMapper.java) |
| moves4 | [physical_activity](https://dev.moves-app.com/docs/api_activities) | [omh:physical-activity:1.2](http://www.openmhealth.org/documentation/#/schema-docs/schema-library/schemas/omh_physical-activity) | [MovesPhysicalActivityDataPointMapper](https://github.com/openmhealth/shimmer/blob/master/shim-server/src/main/java/org/openmhealth/shim/moves/mapper/MovesPhysicalActivityDataPointMapper.java) |
| moves4 | [step_count](https://dev.moves-app.com/docs/api_storyline) | [omh:step-count:1.0](http://www.openmhealth.org/documentation/#/schema-docs/schema-library/schemas/omh_step-count) | [MovesStepCountDataPointMapper](https://github.com/openmhealth/shimmer/blob/master/shim-server/src/main/java/org/openmhealth/shim/moves/mapper/MovesStepCountDataPointMapper.java) |
-| runkeeper | [calories_burned](http://runkeeper.com/developer/healthgraph/fitness-activities#past) | [omh:calories-burned:1.0](http://www.openmhealth.org/documentation/#/schema-docs/schema-library/schemas/omh_calories-burned) | [RunkeeperCaloriesBurnedDataPointMapper](https://github.com/openmhealth/shimmer/blob/master/shim-server/src/main/java/org/openmhealth/shim/runkeeper/mapper/RunkeeperCaloriesBurnedDataPointMapper.java) |
+| runkeeper | [calories_burned](http://runkeeper.com/developer/healthgraph/fitness-activities#past) | [omh:calories-burned:2.0](http://www.openmhealth.org/documentation/#/schema-docs/schema-library/schemas/omh_calories-burned) | [RunkeeperCaloriesBurnedDataPointMapper](https://github.com/openmhealth/shimmer/blob/master/shim-server/src/main/java/org/openmhealth/shim/runkeeper/mapper/RunkeeperCaloriesBurnedDataPointMapper.java) |
| runkeeper | [physical_activity](http://runkeeper.com/developer/healthgraph/fitness-activities#past) | [omh:physical-activity:1.2](http://www.openmhealth.org/documentation/#/schema-docs/schema-library/schemas/omh_physical-activity) | [RunkeeperPhysicalActivityDataPointMapper](https://github.com/openmhealth/shimmer/blob/master/shim-server/src/main/java/org/openmhealth/shim/runkeeper/mapper/RunkeeperPhysicalActivityDataPointMapper.java) |
| withings | [blood_pressure](https://oauth.withings.com/api/doc#api-Measure-get_measure) | [omh:blood-pressure:1.0](http://www.openmhealth.org/documentation/#/schema-docs/schema-library/schemas/omh_blood-pressure) | [WithingsBloodPressureDataPointMapper](https://github.com/openmhealth/shimmer/blob/master/shim-server/src/main/java/org/openmhealth/shim/withings/mapper/WithingsBloodPressureDataPointMapper.java) |
| withings | [body_height](https://oauth.withings.com/api/doc#api-Measure-get_measure) | [omh:body-height:1.0](http://www.openmhealth.org/documentation/#/schema-docs/schema-library/schemas/omh_body-height) | [WithingsBodyHeightDataPointMapper](https://github.com/openmhealth/shimmer/blob/master/shim-server/src/main/java/org/openmhealth/shim/withings/mapper/WithingsBodyHeightDataPointMapper.java) |
| withings | [body_weight](https://oauth.withings.com/api/doc#api-Measure-get_measure) | [omh:body-weight:1.0](http://www.openmhealth.org/documentation/#/schema-docs/schema-library/schemas/omh_body-weight) | [WithingsBodyWeightDataPointMapper](https://github.com/openmhealth/shimmer/blob/master/shim-server/src/main/java/org/openmhealth/shim/withings/mapper/WithingsBodyWeightDataPointMapper.java) |
-| withings | [calories_burned](http://oauth.withings.com/api/doc#api-Measure-get_activity)5 | [omh:calories-burned:1.0](http://www.openmhealth.org/documentation/#/schema-docs/schema-library/schemas/omh_calories-burned) | [WithingsDailyCaloriesBurnedDataPointMapper](https://github.com/openmhealth/shimmer/blob/master/shim-server/src/main/java/org/openmhealth/shim/withings/mapper/WithingsDailyCaloriesBurnedDataPointMapper.java) |
-| withings | [calories_burned](http://oauth.withings.com/api/doc#api-Measure-get_intraday_measure) (intraday)5 | [omh:calories-burned:1.0](http://www.openmhealth.org/documentation/#/schema-docs/schema-library/schemas/omh_calories-burned) | [WithingsIntradayCaloriesBurnedDataPointMapper](https://github.com/openmhealth/shimmer/blob/master/shim-server/src/main/java/org/openmhealth/shim/withings/mapper/WithingsIntradayCaloriesBurnedDataPointMapper.java) |
+| withings | [calories_burned](http://oauth.withings.com/api/doc#api-Measure-get_activity)5 | [omh:calories-burned:2.0](http://www.openmhealth.org/documentation/#/schema-docs/schema-library/schemas/omh_calories-burned) | [WithingsDailyCaloriesBurnedDataPointMapper](https://github.com/openmhealth/shimmer/blob/master/shim-server/src/main/java/org/openmhealth/shim/withings/mapper/WithingsDailyCaloriesBurnedDataPointMapper.java) |
+| withings | [calories_burned](http://oauth.withings.com/api/doc#api-Measure-get_intraday_measure) (intraday)5 | [omh:calories-burned:2.0](http://www.openmhealth.org/documentation/#/schema-docs/schema-library/schemas/omh_calories-burned) | [WithingsIntradayCaloriesBurnedDataPointMapper](https://github.com/openmhealth/shimmer/blob/master/shim-server/src/main/java/org/openmhealth/shim/withings/mapper/WithingsIntradayCaloriesBurnedDataPointMapper.java) |
+| withings | [body_temperature](http://oauth.withings.com/api/doc#api-Measure-get_measure) | [omh:body_temperature:1.0](http://www.openmhealth.org/documentation/#/schema-docs/schema-library/schemas/omh_body-temperature) | [WithingsBodyTemperatureDataPointMapper](https://github.com/openmhealth/shimmer/blob/master/shim-server/src/main/java/org/openmhealth/shim/withings/mapper/WithingsBodyTemperatureDataPointMapper.java) |
| withings | [heart_rate](http://oauth.withings.com/api/doc#api-Measure-get_measure) | [omh:heart-rate:1.1](http://www.openmhealth.org/documentation/#/schema-docs/schema-library/schemas/omh_heart-rate) | [WithingsHeartRateDataPointMapper](https://github.com/openmhealth/shimmer/blob/master/shim-server/src/main/java/org/openmhealth/shim/withings/mapper/WithingsHeartRateDataPointMapper.java) |
-| withings | [sleep_duration](http://oauth.withings.com/api/doc#api-Measure-get_sleep)6 | [omh:sleep-duration:1.0](http://www.openmhealth.org/documentation/#/schema-docs/schema-library/schemas/omh_sleep-duration) | [WithingsSleepDurationDataPointMapper](https://github.com/openmhealth/shimmer/blob/master/shim-server/src/main/java/org/openmhealth/shim/withings/mapper/WithingsSleepDurationDataPointMapper.java) |
-| withings | [step_count](http://oauth.withings.com/api/doc#api-Measure-get_activity)5 | [omh:step-count:1.0](http://www.openmhealth.org/documentation/#/schema-docs/schema-library/schemas/omh_step-count) | [WithingsDailyStepCountDataPointMapper](https://github.com/openmhealth/shimmer/blob/master/shim-server/src/main/java/org/openmhealth/shim/withings/mapper/WithingsDailyStepCountDataPointMapper.java) |
-| withings | [step_count](http://oauth.withings.com/api/doc#api-Measure-get_intraday_measure) (intraday)5 | [omh:step-count:1.0](http://www.openmhealth.org/documentation/#/schema-docs/schema-library/schemas/omh_step-count) | [WithingsIntradayStepCountBurnedDataPointMapper](https://github.com/openmhealth/shimmer/blob/master/shim-server/src/main/java/org/openmhealth/shim/withings/mapper/WithingsIntradayStepCountBurnedDataPointMapper.java) |
+| withings | [sleep_duration](http://oauth.withings.com/api/doc#api-Measure-get_sleep)6 | [omh:sleep-duration:2.0](http://www.openmhealth.org/documentation/#/schema-docs/schema-library/schemas/omh_sleep-duration) | [WithingsSleepDurationDataPointMapper](https://github.com/openmhealth/shimmer/blob/master/shim-server/src/main/java/org/openmhealth/shim/withings/mapper/WithingsSleepDurationDataPointMapper.java) |
+| withings | [sleep_episode](http://oauth.withings.com/api/doc#api-Measure-get_sleep)6 | [omh:sleep-episode:2.0](http://www.openmhealth.org/documentation/#/schema-docs/schema-library/schemas/omh_sleep-episode) | [WithingsSleepEpisodeDataPointMapper](https://github.com/openmhealth/shimmer/blob/master/shim-server/src/main/java/org/openmhealth/shim/withings/mapper/WithingsSleepEpisodeDataPointMapper.java) |
+| withings | [step_count](http://oauth.withings.com/api/doc#api-Measure-get_activity)5 | [omh:step-count:2.0](http://www.openmhealth.org/documentation/#/schema-docs/schema-library/schemas/omh_step-count) | [WithingsDailyStepCountDataPointMapper](https://github.com/openmhealth/shimmer/blob/master/shim-server/src/main/java/org/openmhealth/shim/withings/mapper/WithingsDailyStepCountDataPointMapper.java) |
+| withings | [step_count](http://oauth.withings.com/api/doc#api-Measure-get_intraday_measure) (intraday)5 | [omh:step-count:2.0](http://www.openmhealth.org/documentation/#/schema-docs/schema-library/schemas/omh_step-count) | [WithingsIntradayStepCountBurnedDataPointMapper](https://github.com/openmhealth/shimmer/blob/master/shim-server/src/main/java/org/openmhealth/shim/withings/mapper/WithingsIntradayStepCountBurnedDataPointMapper.java) |
1 *The Fitbit API doesn't 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 contain an incorrect time zone. 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/build.gradle b/build.gradle
index 3a670e1f..d1c044dd 100644
--- a/build.gradle
+++ b/build.gradle
@@ -11,7 +11,7 @@ subprojects {
ext {
javaVersion = 1.8
shimmerVersion = "0.6.0"
- omhSchemaSdkVersion = "1.1.0"
+ omhSchemaSdkVersion = "1.2.1"
}
sourceCompatibility = javaVersion
diff --git a/shim-server/build.gradle b/shim-server/build.gradle
index e88abd0b..e404534f 100644
--- a/shim-server/build.gradle
+++ b/shim-server/build.gradle
@@ -33,6 +33,7 @@ ext {
dependencies {
compile project(":java-shim-sdk")
compile "commons-io:commons-io:2.4"
+ compile "com.google.guava:guava:23.0"
compile "org.hibernate:hibernate-validator"
compile "org.apache.httpcomponents:httpclient"
compile "org.apache.httpcomponents:httpcore"
diff --git a/shim-server/src/main/java/org/openmhealth/shim/CaseStandardizingOAuth2RequestAuthenticator.java b/shim-server/src/main/java/org/openmhealth/shim/CaseStandardizingOAuth2RequestAuthenticator.java
new file mode 100644
index 00000000..34b9319e
--- /dev/null
+++ b/shim-server/src/main/java/org/openmhealth/shim/CaseStandardizingOAuth2RequestAuthenticator.java
@@ -0,0 +1,56 @@
+/*
+ * Copyright 2017 Open mHealth
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.openmhealth.shim;
+
+import org.springframework.http.client.ClientHttpRequest;
+import org.springframework.security.oauth2.client.DefaultOAuth2RequestAuthenticator;
+import org.springframework.security.oauth2.client.OAuth2ClientContext;
+import org.springframework.security.oauth2.client.OAuth2RequestAuthenticator;
+import org.springframework.security.oauth2.client.http.AccessTokenRequiredException;
+import org.springframework.security.oauth2.client.resource.OAuth2ProtectedResourceDetails;
+import org.springframework.security.oauth2.common.OAuth2AccessToken;
+import org.springframework.util.StringUtils;
+
+
+/**
+ * A customization of {@link DefaultOAuth2RequestAuthenticator} that standardizes the case of the Authorization header
+ * token type to "Bearer". This is necessary because the default implementation doesn't work for Moves, which serves up
+ * a "bearer" token but only accepts "Bearer" authorization headers.
+ *
+ * @author Dave Syer
+ * @author Emerson Farrugia
+ */
+public class CaseStandardizingOAuth2RequestAuthenticator implements OAuth2RequestAuthenticator {
+
+ @Override
+ public void authenticate(OAuth2ProtectedResourceDetails resource, OAuth2ClientContext clientContext,
+ ClientHttpRequest request) {
+
+ OAuth2AccessToken accessToken = clientContext.getAccessToken();
+ if (accessToken == null) {
+ throw new AccessTokenRequiredException(resource);
+ }
+
+ String tokenType = accessToken.getTokenType();
+
+ if (!StringUtils.hasText(tokenType) || tokenType.equalsIgnoreCase(OAuth2AccessToken.BEARER_TYPE)) {
+ tokenType = OAuth2AccessToken.BEARER_TYPE; // we'll assume basic bearer token type if none is specified.
+ }
+
+ request.getHeaders().set("Authorization", String.format("%s %s", tokenType, accessToken.getValue()));
+ }
+}
diff --git a/shim-server/src/main/java/org/openmhealth/shim/OAuth2Shim.java b/shim-server/src/main/java/org/openmhealth/shim/OAuth2Shim.java
index bcc71308..8f9e9115 100644
--- a/shim-server/src/main/java/org/openmhealth/shim/OAuth2Shim.java
+++ b/shim-server/src/main/java/org/openmhealth/shim/OAuth2Shim.java
@@ -274,6 +274,8 @@ protected OAuth2RestOperations restTemplate(String stateKey, String code) {
new AccessParameterClientTokenServices(accessParametersRepo));
restTemplate.setAccessTokenProvider(tokenProviderChain);
+ restTemplate.setAuthenticator(new CaseStandardizingOAuth2RequestAuthenticator());
+
return restTemplate;
}
diff --git a/shim-server/src/main/java/org/openmhealth/shim/OptionalStreamSupport.java b/shim-server/src/main/java/org/openmhealth/shim/OptionalStreamSupport.java
new file mode 100644
index 00000000..28f1cf43
--- /dev/null
+++ b/shim-server/src/main/java/org/openmhealth/shim/OptionalStreamSupport.java
@@ -0,0 +1,35 @@
+/*
+ * Copyright 2017 Open mHealth
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.openmhealth.shim;
+
+import java.util.Optional;
+import java.util.stream.Stream;
+
+
+/**
+ * A set of utility methods to help with {@link Optional} {@link Stream} objects.
+ *
+ * @author Emerson Farrugia
+ */
+public class OptionalStreamSupport {
+
+ @SuppressWarnings("OptionalUsedAsFieldOrParameterType")
+ public static Stream asStream(Optional optional) {
+
+ return optional.map(Stream::of).orElseGet(Stream::empty);
+ }
+}
diff --git a/shim-server/src/main/java/org/openmhealth/shim/fitbit/mapper/FitbitSleepMeasureDataPointMapper.java b/shim-server/src/main/java/org/openmhealth/shim/fitbit/mapper/FitbitSleepMeasureDataPointMapper.java
index e5f5f9a0..b41d4629 100644
--- a/shim-server/src/main/java/org/openmhealth/shim/fitbit/mapper/FitbitSleepMeasureDataPointMapper.java
+++ b/shim-server/src/main/java/org/openmhealth/shim/fitbit/mapper/FitbitSleepMeasureDataPointMapper.java
@@ -1,3 +1,19 @@
+/*
+ * Copyright 2017 Open mHealth
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
package org.openmhealth.shim.fitbit.mapper;
import com.fasterxml.jackson.databind.JsonNode;
diff --git a/shim-server/src/main/java/org/openmhealth/shim/googlefit/GoogleFitClientSettings.java b/shim-server/src/main/java/org/openmhealth/shim/googlefit/GoogleFitClientSettings.java
index 56bf4c57..fb3360cb 100644
--- a/shim-server/src/main/java/org/openmhealth/shim/googlefit/GoogleFitClientSettings.java
+++ b/shim-server/src/main/java/org/openmhealth/shim/googlefit/GoogleFitClientSettings.java
@@ -34,7 +34,8 @@ public class GoogleFitClientSettings extends OAuth2ClientSettings {
private List scopes = Arrays.asList(
"https://www.googleapis.com/auth/fitness.activity.read",
- "https://www.googleapis.com/auth/fitness.body.read"
+ "https://www.googleapis.com/auth/fitness.body.read",
+ "https://www.googleapis.com/auth/fitness.location.read"
);
@Override
diff --git a/shim-server/src/main/java/org/openmhealth/shim/googlefit/GoogleFitShim.java b/shim-server/src/main/java/org/openmhealth/shim/googlefit/GoogleFitShim.java
index b2ea660e..00a2bf8d 100644
--- a/shim-server/src/main/java/org/openmhealth/shim/googlefit/GoogleFitShim.java
+++ b/shim-server/src/main/java/org/openmhealth/shim/googlefit/GoogleFitShim.java
@@ -48,7 +48,6 @@
import java.time.ZoneOffset;
import java.util.Map;
-import static java.util.Collections.singletonList;
import static org.slf4j.LoggerFactory.getLogger;
import static org.springframework.http.ResponseEntity.ok;
@@ -114,8 +113,10 @@ public ShimDataType[] getShimDataTypes() {
GoogleFitDataTypes.BODY_HEIGHT,
GoogleFitDataTypes.BODY_WEIGHT,
GoogleFitDataTypes.CALORIES_BURNED,
+ GoogleFitDataTypes.GEOPOSITION,
GoogleFitDataTypes.HEART_RATE,
GoogleFitDataTypes.PHYSICAL_ACTIVITY,
+ GoogleFitDataTypes.SPEED,
GoogleFitDataTypes.STEP_COUNT
};
}
@@ -125,22 +126,21 @@ public enum GoogleFitDataTypes implements ShimDataType {
BODY_HEIGHT("derived:com.google.height:com.google.android.gms:merge_height"),
BODY_WEIGHT("derived:com.google.weight:com.google.android.gms:merge_weight"),
CALORIES_BURNED("derived:com.google.calories.expended:com.google.android.gms:merge_calories_expended"),
+ GEOPOSITION("derived:com.google.location.sample:com.google.android.gms:merge_location_samples"),
HEART_RATE("derived:com.google.heart_rate.bpm:com.google.android.gms:merge_heart_rate_bpm"),
PHYSICAL_ACTIVITY("derived:com.google.activity.segment:com.google.android.gms:merge_activity_segments"),
+ SPEED("derived:com.google.speed:com.google.android.gms:merge_speed"),
STEP_COUNT("derived:com.google.step_count.delta:com.google.android.gms:merge_step_deltas");
private final String streamId;
GoogleFitDataTypes(String streamId) {
-
this.streamId = streamId;
}
public String getStreamId() {
-
return streamId;
}
-
}
protected ResponseEntity getData(OAuth2RestOperations restTemplate,
@@ -157,31 +157,34 @@ protected ResponseEntity getData(OAuth2RestOperations restTemp
+ " in shimDataRequest, cannot retrieve data.");
}
-
OffsetDateTime todayInUTC =
LocalDate.now().atStartOfDay().atOffset(ZoneOffset.UTC);
OffsetDateTime startDateInUTC = shimDataRequest.getStartDateTime() == null ?
+
todayInUTC.minusDays(1) : shimDataRequest.getStartDateTime();
- long startTimeNanos = (startDateInUTC.toEpochSecond() * 1000000000) + startDateInUTC.toInstant().getNano();
+ long startTimeNanos = (startDateInUTC.toEpochSecond() * 1_000_000_000) + startDateInUTC.toInstant().getNano();
OffsetDateTime endDateInUTC = shimDataRequest.getEndDateTime() == null ?
todayInUTC.plusDays(1) :
shimDataRequest.getEndDateTime().plusDays(1); // We are inclusive of the last day, so add 1 day to get
+
// the end of day on the last day, which captures the
// entire last day
- long endTimeNanos = (endDateInUTC.toEpochSecond() * 1000000000) + endDateInUTC.toInstant().getNano();
+ long endTimeNanos = (endDateInUTC.toEpochSecond() * 1_000_000_000) + endDateInUTC.toInstant().getNano();
- UriComponentsBuilder uriBuilder = UriComponentsBuilder.fromUriString(DATA_URL)
- .pathSegment(googleFitDataType.getStreamId(), "datasets", "{startDate}-{endDate}");
// TODO: Add limits back into the request once Google has fixed the 'limit' query parameter and paging
-
- URI uriRequest = uriBuilder.buildAndExpand(startTimeNanos, endTimeNanos).encode().toUri();
+ URI uri = UriComponentsBuilder
+ .fromUriString(DATA_URL)
+ .pathSegment(googleFitDataType.getStreamId(), "datasets", "{startDate}-{endDate}")
+ .buildAndExpand(startTimeNanos, endTimeNanos)
+ .encode()
+ .toUri();
ResponseEntity responseEntity;
try {
- responseEntity = restTemplate.getForEntity(uriRequest, JsonNode.class);
+ responseEntity = restTemplate.getForEntity(uri, JsonNode.class);
}
catch (HttpClientErrorException | HttpServerErrorException e) {
// TODO figure out how to handle this
@@ -190,36 +193,46 @@ protected ResponseEntity getData(OAuth2RestOperations restTemp
}
if (shimDataRequest.getNormalize()) {
- GoogleFitDataPointMapper> dataPointMapper;
- switch (googleFitDataType) {
- case BODY_WEIGHT:
- dataPointMapper = new GoogleFitBodyWeightDataPointMapper();
- break;
- case BODY_HEIGHT:
- dataPointMapper = new GoogleFitBodyHeightDataPointMapper();
- break;
- case PHYSICAL_ACTIVITY:
- dataPointMapper = new GoogleFitPhysicalActivityDataPointMapper();
- break;
- case STEP_COUNT:
- dataPointMapper = new GoogleFitStepCountDataPointMapper();
- break;
- case HEART_RATE:
- dataPointMapper = new GoogleFitHeartRateDataPointMapper();
- break;
- case CALORIES_BURNED:
- dataPointMapper = new GoogleFitCaloriesBurnedDataPointMapper();
- break;
- default:
- throw new UnsupportedOperationException();
- }
+ GoogleFitDataPointMapper> dataPointMapper = getDataPointMapper(googleFitDataType);
- return ok().body(ShimDataResponse.result(GoogleFitShim.SHIM_KEY, dataPointMapper.asDataPoints(
- singletonList(responseEntity.getBody()))));
+ return ok().body(ShimDataResponse
+ .result(GoogleFitShim.SHIM_KEY, dataPointMapper.asDataPoints(responseEntity.getBody())));
}
else {
+ return ok().body(ShimDataResponse
+ .result(GoogleFitShim.SHIM_KEY, responseEntity.getBody()));
+ }
+ }
+
+ private GoogleFitDataPointMapper> getDataPointMapper(GoogleFitDataTypes googleFitDataType) {
+
+ switch (googleFitDataType) {
+ case BODY_HEIGHT:
+ return new GoogleFitBodyHeightDataPointMapper();
+
+ case BODY_WEIGHT:
+ return new GoogleFitBodyWeightDataPointMapper();
+
+ case CALORIES_BURNED:
+ return new GoogleFitCaloriesBurnedDataPointMapper();
+
+ case GEOPOSITION:
+ return new GoogleFitGeopositionDataPointMapper();
+
+ case HEART_RATE:
+ return new GoogleFitHeartRateDataPointMapper();
+
+ case PHYSICAL_ACTIVITY:
+ return new GoogleFitPhysicalActivityDataPointMapper();
+
+ case SPEED:
+ return new GoogleFitSpeedDataPointMapper();
+
+ case STEP_COUNT:
+ return new GoogleFitStepCountDataPointMapper();
- return ok().body(ShimDataResponse.result(GoogleFitShim.SHIM_KEY, responseEntity.getBody()));
+ default:
+ throw new UnsupportedOperationException();
}
}
diff --git a/shim-server/src/main/java/org/openmhealth/shim/googlefit/mapper/GoogleFitBodyHeightDataPointMapper.java b/shim-server/src/main/java/org/openmhealth/shim/googlefit/mapper/GoogleFitBodyHeightDataPointMapper.java
index 990e1a31..55e37b6e 100644
--- a/shim-server/src/main/java/org/openmhealth/shim/googlefit/mapper/GoogleFitBodyHeightDataPointMapper.java
+++ b/shim-server/src/main/java/org/openmhealth/shim/googlefit/mapper/GoogleFitBodyHeightDataPointMapper.java
@@ -46,11 +46,11 @@ public Optional> asDataPoint(JsonNode listNode) {
return Optional.empty();
}
- BodyHeight.Builder bodyHeightBuilder = new BodyHeight.Builder(new LengthUnitValue(METER, bodyHeightValue));
+ BodyHeight.Builder measureBuilder = new BodyHeight.Builder(new LengthUnitValue(METER, bodyHeightValue));
- setEffectiveTimeFrameIfPresent(bodyHeightBuilder, listNode);
+ getOptionalTimeFrame(listNode).ifPresent(measureBuilder::setEffectiveTimeFrame);
- BodyHeight bodyHeight = bodyHeightBuilder.build();
+ BodyHeight bodyHeight = measureBuilder.build();
Optional originDataSourceId = asOptionalString(listNode, "originDataSourceId");
return Optional.of(newDataPoint(bodyHeight, originDataSourceId.orElse(null)));
diff --git a/shim-server/src/main/java/org/openmhealth/shim/googlefit/mapper/GoogleFitBodyWeightDataPointMapper.java b/shim-server/src/main/java/org/openmhealth/shim/googlefit/mapper/GoogleFitBodyWeightDataPointMapper.java
index d81465de..4fcbd79f 100644
--- a/shim-server/src/main/java/org/openmhealth/shim/googlefit/mapper/GoogleFitBodyWeightDataPointMapper.java
+++ b/shim-server/src/main/java/org/openmhealth/shim/googlefit/mapper/GoogleFitBodyWeightDataPointMapper.java
@@ -47,12 +47,13 @@ public Optional> asDataPoint(JsonNode listNode) {
return Optional.empty();
}
- BodyWeight.Builder bodyWeightBuilder = new BodyWeight.Builder(new MassUnitValue(KILOGRAM, bodyWeightValue));
- setEffectiveTimeFrameIfPresent(bodyWeightBuilder, listNode);
+ BodyWeight.Builder measureBuilder = new BodyWeight.Builder(new MassUnitValue(KILOGRAM, bodyWeightValue));
+
+ getOptionalTimeFrame(listNode).ifPresent(measureBuilder::setEffectiveTimeFrame);
Optional originDataSourceId = asOptionalString(listNode, "originDataSourceId");
- BodyWeight bodyWeight = bodyWeightBuilder.build();
+ BodyWeight bodyWeight = measureBuilder.build();
return Optional.of(newDataPoint(bodyWeight, originDataSourceId.orElse(null)));
}
}
diff --git a/shim-server/src/main/java/org/openmhealth/shim/googlefit/mapper/GoogleFitCaloriesBurnedDataPointMapper.java b/shim-server/src/main/java/org/openmhealth/shim/googlefit/mapper/GoogleFitCaloriesBurnedDataPointMapper.java
index 2f3dc36a..e9c4838f 100644
--- a/shim-server/src/main/java/org/openmhealth/shim/googlefit/mapper/GoogleFitCaloriesBurnedDataPointMapper.java
+++ b/shim-server/src/main/java/org/openmhealth/shim/googlefit/mapper/GoogleFitCaloriesBurnedDataPointMapper.java
@@ -17,9 +17,8 @@
package org.openmhealth.shim.googlefit.mapper;
import com.fasterxml.jackson.databind.JsonNode;
-import org.openmhealth.schema.domain.omh.CaloriesBurned;
+import org.openmhealth.schema.domain.omh.CaloriesBurned2;
import org.openmhealth.schema.domain.omh.DataPoint;
-import org.openmhealth.schema.domain.omh.KcalUnitValue;
import java.util.Optional;
@@ -28,32 +27,29 @@
/**
- * A mapper from Google Fit "merged calories expended" endpoint responses
- * (derived:com.google.calories.expended:com.google.android.gms:merge_calories_expended) to {@link CaloriesBurned}
- * objects.
+ * A mapper from Google Fit "merged calories expended" endpoint responses (derived:com.google.calories.expended:com.google.android.gms:merge_calories_expended)
+ * to {@link CaloriesBurned2} objects.
*
* @author Chris Schaefbauer
* @see Google Fit Data Type Documentation
*/
-public class GoogleFitCaloriesBurnedDataPointMapper extends GoogleFitDataPointMapper {
+public class GoogleFitCaloriesBurnedDataPointMapper extends GoogleFitDataPointMapper {
@Override
- protected Optional> asDataPoint(JsonNode listNode) {
+ protected Optional> asDataPoint(JsonNode listNode) {
JsonNode listValueNode = asRequiredNode(listNode, "value");
- // TODO isn't this just "value.fpVal"?
double caloriesBurnedValue = asRequiredDouble(listValueNode.get(0), "fpVal");
- CaloriesBurned.Builder caloriesBurnedBuilder =
- new CaloriesBurned.Builder(new KcalUnitValue(KILOCALORIE, caloriesBurnedValue));
+ CaloriesBurned2.Builder measureBuilder =
+ new CaloriesBurned2.Builder(KILOCALORIE.newUnitValue(caloriesBurnedValue), getTimeFrame(listNode));
- setEffectiveTimeFrameIfPresent(caloriesBurnedBuilder, listNode);
+ CaloriesBurned2 caloriesBurned = measureBuilder.build();
- CaloriesBurned caloriesBurned = caloriesBurnedBuilder.build();
Optional originDataSourceId = asOptionalString(listNode, "originDataSourceId");
// Google Fit calories burned endpoint returns calories burned by basal metabolic rate (BMR), however these
- // are not activity related calories burned so we do not create a datapoint for values from this source
+ // are not activity related calories burned so we do not create a data point for values from this source
if (originDataSourceId.isPresent()) {
if (originDataSourceId.get().contains("bmr")) {
return Optional.empty();
diff --git a/shim-server/src/main/java/org/openmhealth/shim/googlefit/mapper/GoogleFitDataPointMapper.java b/shim-server/src/main/java/org/openmhealth/shim/googlefit/mapper/GoogleFitDataPointMapper.java
index 952271f2..4f97d62d 100644
--- a/shim-server/src/main/java/org/openmhealth/shim/googlefit/mapper/GoogleFitDataPointMapper.java
+++ b/shim-server/src/main/java/org/openmhealth/shim/googlefit/mapper/GoogleFitDataPointMapper.java
@@ -20,16 +20,20 @@
import com.google.common.collect.Lists;
import org.openmhealth.schema.domain.omh.*;
import org.openmhealth.shim.common.mapper.JsonNodeDataPointMapper;
+import org.openmhealth.shim.common.mapper.MissingJsonNodeMappingException;
import java.time.Instant;
import java.time.OffsetDateTime;
-import java.time.ZoneId;
import java.util.List;
import java.util.Optional;
import java.util.UUID;
import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.base.Preconditions.checkNotNull;
+import static java.lang.Long.parseLong;
+import static java.time.ZoneOffset.UTC;
+import static java.util.Optional.empty;
+import static org.openmhealth.schema.domain.omh.TimeInterval.ofStartDateTimeAndEndDateTime;
import static org.openmhealth.shim.common.mapper.JsonNodeMappingSupport.asOptionalNode;
import static org.openmhealth.shim.common.mapper.JsonNodeMappingSupport.asOptionalString;
@@ -43,6 +47,9 @@ public abstract class GoogleFitDataPointMapper implements Jso
public static final String RESOURCE_API_SOURCE_NAME = "Google Fit API";
+ private static final String EPOCH_NS_START_DATE_TIME_PATH = "startTimeNanos";
+ private static final String EPOCH_NS_END_DATE_TIME_PATH = "endTimeNanos";
+
/**
* Maps a JSON response from the Google Fit API containing a JSON array of data points to a list of {@link
* DataPoint} objects of the appropriate measure type. Splits individual nodes based on the name of the list node,
@@ -67,7 +74,6 @@ public List> asDataPoints(List responseNodes) {
}
return dataPoints;
-
}
/**
@@ -114,40 +120,52 @@ public DataPoint newDataPoint(T measure, String fitDataSourceId) {
}
/**
- * Converts a nanosecond timestamp from the Google Fit API into an offset datetime value.
+ * Converts a nanosecond timestamp from the Google Fit API into an offset date time value.
*
- * @param unixEpochNanosString the timestamp directly from the Google JSON document
+ * @param epochNanosecondString the timestamp directly from the Google JSON document
* @return an offset datetime object representing the input timestamp
*/
- public OffsetDateTime convertGoogleNanosToOffsetDateTime(String unixEpochNanosString) {
+ private OffsetDateTime asOffsetDateTime(String epochNanosecondString) {
- return OffsetDateTime.ofInstant(Instant.ofEpochSecond(0, Long.parseLong(unixEpochNanosString)), ZoneId.of("Z"));
+ return OffsetDateTime.ofInstant(Instant.ofEpochSecond(0, parseLong(epochNanosecondString)), UTC);
}
/**
- * @param builder a measure builder of type T
- * @param listNode the JSON node representing an individual datapoint, which contains the start and end time
- * properties, from within the response array
+ * @param node a JSON node optionally containing time frame properties
*/
- public void setEffectiveTimeFrameIfPresent(T.Builder builder, JsonNode listNode) {
+ public Optional getOptionalTimeFrame(JsonNode node) {
- Optional startTimeNanosString = asOptionalString(listNode, "startTimeNanos");
- Optional endTimeNanosString = asOptionalString(listNode, "endTimeNanos");
+ Optional startTimeNanosString = asOptionalString(node, EPOCH_NS_START_DATE_TIME_PATH);
+ Optional endTimeNanosString = asOptionalString(node, EPOCH_NS_END_DATE_TIME_PATH);
// When the start and end times are identical, such as for a single body weight measure, then we only need to
// create an effective time frame with a single date time value
if (startTimeNanosString.isPresent() && endTimeNanosString.isPresent()) {
if (startTimeNanosString.equals(endTimeNanosString)) {
- builder.setEffectiveTimeFrame(convertGoogleNanosToOffsetDateTime(startTimeNanosString.get()));
-
+ return Optional.of(new TimeFrame(asOffsetDateTime(startTimeNanosString.get())));
}
else {
- builder.setEffectiveTimeFrame(TimeInterval.ofStartDateTimeAndEndDateTime(
- convertGoogleNanosToOffsetDateTime(startTimeNanosString.get()),
- convertGoogleNanosToOffsetDateTime(endTimeNanosString.get())));
+ return Optional.of(new TimeFrame(ofStartDateTimeAndEndDateTime(
+ asOffsetDateTime(startTimeNanosString.get()),
+ asOffsetDateTime(endTimeNanosString.get()))
+ ));
}
-
}
+
+ return empty();
+ }
+
+ public TimeFrame getTimeFrame(JsonNode node) {
+
+ return getOptionalTimeFrame(node)
+ .orElseThrow(() -> {
+ if (!asOptionalString(node, EPOCH_NS_START_DATE_TIME_PATH).isPresent()) {
+ return new MissingJsonNodeMappingException(node, EPOCH_NS_START_DATE_TIME_PATH);
+ }
+ else {
+ return new MissingJsonNodeMappingException(node, EPOCH_NS_END_DATE_TIME_PATH);
+ }
+ });
}
/**
@@ -163,5 +181,4 @@ protected String getListNodeName() {
protected String getValueListNodeName() {
return "value";
}
-
}
diff --git a/shim-server/src/main/java/org/openmhealth/shim/googlefit/mapper/GoogleFitGeopositionDataPointMapper.java b/shim-server/src/main/java/org/openmhealth/shim/googlefit/mapper/GoogleFitGeopositionDataPointMapper.java
new file mode 100644
index 00000000..79873604
--- /dev/null
+++ b/shim-server/src/main/java/org/openmhealth/shim/googlefit/mapper/GoogleFitGeopositionDataPointMapper.java
@@ -0,0 +1,64 @@
+/*
+ * Copyright 2017 Open mHealth
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.openmhealth.shim.googlefit.mapper;
+
+import com.fasterxml.jackson.databind.JsonNode;
+import org.openmhealth.schema.domain.omh.DataPoint;
+import org.openmhealth.schema.domain.omh.Geoposition;
+
+import java.util.Optional;
+
+import static org.openmhealth.schema.domain.omh.LengthUnit.METER;
+import static org.openmhealth.schema.domain.omh.PlaneAngleUnit.DEGREE_OF_ARC;
+import static org.openmhealth.shim.common.mapper.JsonNodeMappingSupport.*;
+
+
+/**
+ * A mapper from Google Fit "merged location samples" endpoint responses (derived:com.google.location.sample:com.google.android.gms:merge_location_samples)
+ * to {@link Geoposition} objects.
+ *
+ * @author Emerson Farrugia
+ * @see Google Fit Data Type Documentation
+ */
+public class GoogleFitGeopositionDataPointMapper extends GoogleFitDataPointMapper {
+
+ @Override
+ protected Optional> asDataPoint(JsonNode listNode) {
+
+ JsonNode listValueNode = asRequiredNode(listNode, "value");
+ double latitude = asRequiredDouble(listValueNode.get(0), "fpVal");
+ double longitude = asRequiredDouble(listValueNode.get(1), "fpVal");
+ // TODO add accuracy to geoposition
+ Optional accuracyInM = asOptionalDouble(listValueNode.get(2), "fpVal");
+
+ Geoposition.Builder measureBuilder =
+ new Geoposition.Builder(
+ DEGREE_OF_ARC.newUnitValue(latitude),
+ DEGREE_OF_ARC.newUnitValue(longitude),
+ getTimeFrame(listNode));
+
+ if (listValueNode.size() >= 4) {
+ measureBuilder.setElevation(METER.newUnitValue(asRequiredDouble(listValueNode.get(3), "fpVal")));
+ }
+
+ Geoposition geoposition = measureBuilder.build();
+
+ Optional originDataSourceId = asOptionalString(listNode, "originDataSourceId");
+
+ return Optional.of(newDataPoint(geoposition, originDataSourceId.orElse(null)));
+ }
+}
diff --git a/shim-server/src/main/java/org/openmhealth/shim/googlefit/mapper/GoogleFitHeartRateDataPointMapper.java b/shim-server/src/main/java/org/openmhealth/shim/googlefit/mapper/GoogleFitHeartRateDataPointMapper.java
index ee715492..ba2bd9c6 100644
--- a/shim-server/src/main/java/org/openmhealth/shim/googlefit/mapper/GoogleFitHeartRateDataPointMapper.java
+++ b/shim-server/src/main/java/org/openmhealth/shim/googlefit/mapper/GoogleFitHeartRateDataPointMapper.java
@@ -39,13 +39,18 @@ protected Optional> asDataPoint(JsonNode listNode) {
JsonNode valueListNode = asRequiredNode(listNode, "value");
double heartRateValue = asRequiredDouble(valueListNode.get(0), "fpVal");
+
if (heartRateValue == 0) {
return Optional.empty();
}
- HeartRate.Builder heartRateBuilder = new HeartRate.Builder(heartRateValue);
- setEffectiveTimeFrameIfPresent(heartRateBuilder, listNode);
- HeartRate heartRate = heartRateBuilder.build();
+
+ HeartRate.Builder measureBuilder = new HeartRate.Builder(heartRateValue);
+
+ getOptionalTimeFrame(listNode).ifPresent(measureBuilder::setEffectiveTimeFrame);
+
+ HeartRate heartRate = measureBuilder.build();
Optional originDataSourceId = asOptionalString(listNode, "originDataSourceId");
+
return Optional.of(newDataPoint(heartRate, originDataSourceId.orElse(null)));
}
}
diff --git a/shim-server/src/main/java/org/openmhealth/shim/googlefit/mapper/GoogleFitPhysicalActivityDataPointMapper.java b/shim-server/src/main/java/org/openmhealth/shim/googlefit/mapper/GoogleFitPhysicalActivityDataPointMapper.java
index fe5ce23c..59beb4f0 100644
--- a/shim-server/src/main/java/org/openmhealth/shim/googlefit/mapper/GoogleFitPhysicalActivityDataPointMapper.java
+++ b/shim-server/src/main/java/org/openmhealth/shim/googlefit/mapper/GoogleFitPhysicalActivityDataPointMapper.java
@@ -68,13 +68,14 @@ protected Optional> asDataPoint(JsonNode listNode) {
}
String activityName = googleFitDataTypes.get((int) activityTypeId);
- PhysicalActivity.Builder physicalActivityBuilder = new PhysicalActivity.Builder(
- activityName);
- setEffectiveTimeFrameIfPresent(physicalActivityBuilder, listNode);
- PhysicalActivity physicalActivity = physicalActivityBuilder.build();
+ PhysicalActivity.Builder measureBuilder = new PhysicalActivity.Builder(activityName);
+
+ getOptionalTimeFrame(listNode).ifPresent(measureBuilder::setEffectiveTimeFrame);
+
+ PhysicalActivity physicalActivity = measureBuilder.build();
Optional originSourceId = asOptionalString(listNode, "originDataSourceId");
- return Optional.of(newDataPoint(physicalActivity, originSourceId.orElse(null)));
+ return Optional.of(newDataPoint(physicalActivity, originSourceId.orElse(null)));
}
/**
@@ -205,7 +206,7 @@ private void initializeActivityMap() {
.put(110, "Deep sleep")
.put(111, "REM sleep")
.put(112, "Awake (during sleep cycle)");
+
googleFitDataTypes = activityDataTypeBuilder.build();
}
-
}
diff --git a/shim-server/src/main/java/org/openmhealth/shim/googlefit/mapper/GoogleFitSpeedDataPointMapper.java b/shim-server/src/main/java/org/openmhealth/shim/googlefit/mapper/GoogleFitSpeedDataPointMapper.java
new file mode 100644
index 00000000..92c7c61f
--- /dev/null
+++ b/shim-server/src/main/java/org/openmhealth/shim/googlefit/mapper/GoogleFitSpeedDataPointMapper.java
@@ -0,0 +1,50 @@
+/*
+ * Copyright 2017 Open mHealth
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.openmhealth.shim.googlefit.mapper;
+
+import com.fasterxml.jackson.databind.JsonNode;
+import org.openmhealth.schema.domain.omh.DataPoint;
+import org.openmhealth.schema.domain.omh.Speed;
+
+import java.util.Optional;
+
+import static org.openmhealth.schema.domain.omh.SpeedUnit.METERS_PER_SECOND;
+import static org.openmhealth.shim.common.mapper.JsonNodeMappingSupport.*;
+
+
+/**
+ * A mapper from Google Fit "merged speed" endpoint responses (derived:com.google.speed:com.google.android.gms:merge_speed)
+ * to {@link Speed} objects.
+ *
+ * @author Emerson Farrugia
+ * @see Google Fit Data Type Documentation
+ */
+public class GoogleFitSpeedDataPointMapper extends GoogleFitDataPointMapper {
+
+ @Override
+ protected Optional> asDataPoint(JsonNode listNode) {
+
+ JsonNode listValueNode = asRequiredNode(listNode, "value");
+ double speedValue = asRequiredDouble(listValueNode.get(0), "fpVal");
+
+ Speed speed = new Speed.Builder(METERS_PER_SECOND.newUnitValue(speedValue), getTimeFrame(listNode)).build();
+
+ Optional originDataSourceId = asOptionalString(listNode, "originDataSourceId");
+
+ return Optional.of(newDataPoint(speed, originDataSourceId.orElse(null)));
+ }
+}
diff --git a/shim-server/src/main/java/org/openmhealth/shim/googlefit/mapper/GoogleFitStepCountDataPointMapper.java b/shim-server/src/main/java/org/openmhealth/shim/googlefit/mapper/GoogleFitStepCountDataPointMapper.java
index c7685452..cf4f3f4f 100644
--- a/shim-server/src/main/java/org/openmhealth/shim/googlefit/mapper/GoogleFitStepCountDataPointMapper.java
+++ b/shim-server/src/main/java/org/openmhealth/shim/googlefit/mapper/GoogleFitStepCountDataPointMapper.java
@@ -18,7 +18,7 @@
import com.fasterxml.jackson.databind.JsonNode;
import org.openmhealth.schema.domain.omh.DataPoint;
-import org.openmhealth.schema.domain.omh.StepCount1;
+import org.openmhealth.schema.domain.omh.StepCount2;
import java.util.Optional;
@@ -26,27 +26,30 @@
/**
- * A mapper from Google Fit "merged step count delta" endpoint responses
- * (derived:com.google.step_count.delta:com.google.android.gms:merge_step_deltas) to {@link StepCount1}
- * objects.
+ * A mapper from Google Fit "merged step count delta" endpoint responses (derived:com.google.step_count.delta:com.google.android.gms:merge_step_deltas)
+ * to {@link StepCount2} objects.
*
* @author Chris Schaefbauer
+ * @author Emerson Farrugia
* @see Google Fit Data Type Documentation
*/
-public class GoogleFitStepCountDataPointMapper extends GoogleFitDataPointMapper {
+public class GoogleFitStepCountDataPointMapper extends GoogleFitDataPointMapper {
@Override
- protected Optional> asDataPoint(JsonNode listNode) {
+ protected Optional> asDataPoint(JsonNode listNode) {
JsonNode listValueNode = asRequiredNode(listNode, "value");
long stepCountValue = asRequiredLong(listValueNode.get(0), "intVal");
+
if (stepCountValue == 0) {
return Optional.empty();
}
- StepCount1.Builder stepCountBuilder = new StepCount1.Builder(stepCountValue);
- setEffectiveTimeFrameIfPresent(stepCountBuilder, listNode);
- StepCount1 stepCount = stepCountBuilder.build();
+
+ StepCount2 stepCount = new StepCount2.Builder(stepCountValue, getTimeFrame(listNode))
+ .build();
+
Optional originSourceId = asOptionalString(listNode, "originDataSourceId");
+
return Optional.of(newDataPoint(stepCount, originSourceId.orElse(null)));
}
}
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
index ca64cf8b..98b42e04 100644
--- a/shim-server/src/main/java/org/openmhealth/shim/ihealth/IHealthShim.java
+++ b/shim-server/src/main/java/org/openmhealth/shim/ihealth/IHealthShim.java
@@ -22,6 +22,7 @@
import com.google.common.base.Joiner;
import com.google.common.base.Preconditions;
import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.Lists;
import org.openmhealth.shim.*;
import org.openmhealth.shim.ihealth.mapper.*;
import org.slf4j.Logger;
@@ -134,24 +135,29 @@ public ShimDataType[] getShimDataTypes() {
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")),
- OXYGEN_SATURATION(singletonList("spo2.json"));
+ PHYSICAL_ACTIVITY("sport.json"),
+ BLOOD_GLUCOSE("glucose.json"),
+ BLOOD_PRESSURE("bp.json"),
+ BODY_WEIGHT("weight.json"),
+ BODY_MASS_INDEX("weight.json"),
+ HEART_RATE("bp.json", "spo2.json"),
+ STEP_COUNT("activity.json"),
+ SLEEP_DURATION("sleep.json"),
+ OXYGEN_SATURATION("spo2.json");
private List endPoint;
- IHealthDataTypes(List endPoint) {
+ IHealthDataTypes(String endpoint) {
- this.endPoint = endPoint;
+ this.endPoint = singletonList(endpoint);
}
- public List getEndPoint() {
+ IHealthDataTypes(String... endpoints) {
+
+ this.endPoint = Lists.newArrayList(endpoints);
+ }
+
+ public List getEndpoints() {
return endPoint;
}
@@ -198,7 +204,7 @@ protected ResponseEntity getData(OAuth2RestOperations restTemp
// 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()) {
+ for (String endpoint : dataType.getEndpoints()) {
UriComponentsBuilder uriBuilder = UriComponentsBuilder.fromUriString(clientSettings.getApiBaseUrl() + "/");
@@ -217,7 +223,7 @@ protected ResponseEntity getData(OAuth2RestOperations restTemp
uriBuilder.path("/user/")
.path(userId + "/")
- .path(endPoint)
+ .path(endpoint)
.queryParam("client_id", restTemplate.getResource().getClientId())
.queryParam("client_secret", restTemplate.getResource().getClientSecret())
.queryParam("start_time", startDate.toEpochSecond())
@@ -268,11 +274,11 @@ protected ResponseEntity getData(OAuth2RestOperations restTemp
break;
case HEART_RATE:
// there are two different mappers for heart rate because the data can come from two endpoints
- if (endPoint == "bp.json") {
+ if (endpoint.equals("bp.json")) {
mapper = new IHealthBloodPressureEndpointHeartRateDataPointMapper();
break;
}
- else if (endPoint == "spo2.json") {
+ else if (endpoint.equals("spo2.json")) {
mapper = new IHealthBloodOxygenEndpointHeartRateDataPointMapper();
break;
}
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
index 08e4c686..24964939 100644
--- 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
@@ -105,7 +105,6 @@ protected DataPointHeader createDataPointHeader(JsonNode listEntryNode, Measure
return new DataPointHeader.Builder(UUID.randomUUID().toString(), measure.getSchemaId())
.setAcquisitionProvenance(acquisitionProvenance)
.build();
-
}
/**
@@ -117,9 +116,9 @@ protected DataPointHeader createDataPointHeader(JsonNode listEntryNode, Measure
*/
protected static Optional getEffectiveTimeFrameAsDateTime(JsonNode listEntryNode) {
- Optional weirdSeconds = asOptionalLong(listEntryNode, "MDate");
+ Optional epochSecondsInLocalTime = asOptionalLong(listEntryNode, "MDate");
- if (!weirdSeconds.isPresent()) {
+ if (!epochSecondsInLocalTime.isPresent()) {
return Optional.empty();
}
@@ -152,14 +151,14 @@ else if (asOptionalLong(listEntryNode, "TimeZone").isPresent()) {
return Optional.empty();
}
- return Optional.of(new TimeFrame(getDateTimeWithCorrectOffset(weirdSeconds.get(), zoneOffset)));
+ return Optional.of(new TimeFrame(getDateTimeWithCorrectOffset(epochSecondsInLocalTime.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.
+ * 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) {
@@ -173,20 +172,19 @@ protected static OffsetDateTime getDateTimeWithCorrectOffset(Long localTimeAsEpo
}
/**
- * @param dateTimeInUnixSecondsWithLocalTimeOffset A unix epoch timestamp in local time.
+ * @param epochSecondsInLocalTime 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) {
+ Long epochSecondsInLocalTime,
+ 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"));
+ OffsetDateTime dateTimeFromInstant = ofInstant(ofEpochSecond(epochSecondsInLocalTime), ZoneId.of("Z"));
- return dateTimeFromOffsetInstant.toLocalDate().atStartOfDay().atOffset(ZoneOffset.of(timeZoneString));
+ return dateTimeFromInstant.toLocalDate().atStartOfDay().atOffset(ZoneOffset.of(timeZoneString));
}
/**
@@ -207,7 +205,7 @@ protected static Optional getUserNoteIfExists(JsonNode listEntryNode) {
}
/**
- * Sets the correct DataPointModality based on the iHealth value indicating the source of the DataPoint.
+ * Sets the correct modality based on the iHealth value indicating the source of the data point.
*
* @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.
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
index 08705cb5..422860c5 100644
--- 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
@@ -17,24 +17,29 @@
package org.openmhealth.shim.ihealth.mapper;
import com.fasterxml.jackson.databind.JsonNode;
-import org.openmhealth.schema.domain.omh.*;
+import org.openmhealth.schema.domain.omh.DataPoint;
+import org.openmhealth.schema.domain.omh.DurationUnitValue;
+import org.openmhealth.schema.domain.omh.SleepDuration2;
+import org.openmhealth.schema.domain.omh.TimeInterval;
import java.time.ZoneOffset;
import java.util.Optional;
-import static org.openmhealth.schema.domain.omh.TimeInterval.*;
+import static org.openmhealth.schema.domain.omh.DurationUnit.MINUTE;
+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 /sleep.json endpoint into {@link SleepDuration1}
+ * A mapper that translates responses from the iHealth /sleep.json endpoint into {@link SleepDuration2}
* measures.
*
* @author Chris Schaefbauer
+ * @author Emerson Farrugia
* @see endpoint
* documentation
*/
-public class IHealthSleepDurationDataPointMapper extends IHealthDataPointMapper {
+public class IHealthSleepDurationDataPointMapper extends IHealthDataPointMapper {
@Override
protected String getListNodeName() {
@@ -47,32 +52,24 @@ protected Optional getMeasureUnitNodeName() {
}
@Override
- protected Optional> asDataPoint(JsonNode listEntryNode, Integer measureUnitMagicNumber) {
+ protected Optional> asDataPoint(JsonNode listEntryNode, Integer measureUnitMagicNumber) {
- SleepDuration1.Builder sleepDurationBuilder = new SleepDuration1.Builder(
- new DurationUnitValue(DurationUnit.MINUTE, asRequiredBigDecimal(listEntryNode, "HoursSlept")));
+ Long effectiveStartEpochSecondsInLocalTime = asRequiredLong(listEntryNode, "StartTime");
+ Long effectiveEndEpochSecondsInLocalTime = asRequiredLong(listEntryNode, "EndTime");
+ ZoneOffset effectiveTimeZoneOffset = ZoneOffset.of(asRequiredString(listEntryNode, "TimeZone"));
- Optional startTime = asOptionalLong(listEntryNode, "StartTime");
- Optional endTime = asOptionalLong(listEntryNode, "EndTime");
+ TimeInterval effectiveTimeInterval = ofStartDateTimeAndEndDateTime(
+ getDateTimeWithCorrectOffset(effectiveStartEpochSecondsInLocalTime, effectiveTimeZoneOffset),
+ getDateTimeWithCorrectOffset(effectiveEndEpochSecondsInLocalTime, effectiveTimeZoneOffset));
- 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()))));
- }
- }
+ SleepDuration2.Builder sleepDurationBuilder = new SleepDuration2.Builder(
+ // property is called HoursSlept but it's in minutes
+ new DurationUnitValue(MINUTE, asRequiredBigDecimal(listEntryNode, "HoursSlept")),
+ effectiveTimeInterval);
getUserNoteIfExists(listEntryNode).ifPresent(sleepDurationBuilder::setUserNotes);
- SleepDuration1 sleepDuration = sleepDurationBuilder.build();
-
- asOptionalBigDecimal(listEntryNode, "Awaken")
- .ifPresent(awaken -> sleepDuration.setAdditionalProperty("wakeup_count", awaken));
+ SleepDuration2 sleepDuration = sleepDurationBuilder.build();
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
index a5fabb7a..30e585d1 100644
--- 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
@@ -19,7 +19,8 @@
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.StepCount1;
+import org.openmhealth.schema.domain.omh.StepCount2;
+import org.openmhealth.schema.domain.omh.TimeInterval;
import java.math.BigDecimal;
import java.util.Optional;
@@ -30,14 +31,14 @@
/**
- * A mapper that translates responses from the iHealth /activity.json endpoint into {@link StepCount1}
+ * A mapper that translates responses from the iHealth /activity.json endpoint into {@link StepCount2}
* measures.
*
* @author Chris Schaefbauer
* @see endpoint
* documentation
*/
-public class IHealthStepCountDataPointMapper extends IHealthDataPointMapper {
+public class IHealthStepCountDataPointMapper extends IHealthDataPointMapper {
@Override
protected String getListNodeName() {
@@ -50,7 +51,7 @@ protected Optional getMeasureUnitNodeName() {
}
@Override
- protected Optional> asDataPoint(JsonNode listEntryNode, Integer measureUnitMagicNumber) {
+ protected Optional> asDataPoint(JsonNode listEntryNode, Integer measureUnitMagicNumber) {
BigDecimal steps = asRequiredBigDecimal(listEntryNode, "Steps");
@@ -58,30 +59,21 @@ protected Optional> asDataPoint(JsonNode listEntryNode, In
return Optional.empty();
}
- StepCount1.Builder stepCountBuilder = new StepCount1.Builder(steps);
+ Long effectiveEpochSecondsInLocalTime = asRequiredLong(listEntryNode, "MDate");
+ String effectiveTimeZoneOffset = asRequiredString(listEntryNode, "TimeZone");
- Optional dateTimeString = asOptionalLong(listEntryNode, "MDate");
+ /* iHealth provides daily summaries for step counts and timestamp the data point at either the end of
+ the day (23:50) or at the latest time that data point was synced. */
+ TimeInterval effectiveTimeInterval = ofStartDateTimeAndDuration(
+ getDateTimeAtStartOfDayWithCorrectOffset(effectiveEpochSecondsInLocalTime, effectiveTimeZoneOffset),
+ new DurationUnitValue(DAY, 1));
- if (dateTimeString.isPresent()) {
+ StepCount2.Builder measureBuilder = new StepCount2.Builder(steps, effectiveTimeInterval);
- Optional timeZone = asOptionalString(listEntryNode, "TimeZone");
+ getUserNoteIfExists(listEntryNode).ifPresent(measureBuilder::setUserNotes);
- 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);
-
- StepCount1 stepCount = stepCountBuilder.build();
+ StepCount2 stepCount = measureBuilder.build();
return Optional.of(new DataPoint<>(createDataPointHeader(listEntryNode, stepCount), stepCount));
}
-
-
}
diff --git a/shim-server/src/main/java/org/openmhealth/shim/misfit/MisfitShim.java b/shim-server/src/main/java/org/openmhealth/shim/misfit/MisfitShim.java
index c443f6d3..74ec4a7d 100644
--- a/shim-server/src/main/java/org/openmhealth/shim/misfit/MisfitShim.java
+++ b/shim-server/src/main/java/org/openmhealth/shim/misfit/MisfitShim.java
@@ -21,10 +21,7 @@
import com.google.common.base.Joiner;
import com.google.common.base.Splitter;
import org.openmhealth.shim.*;
-import org.openmhealth.shim.misfit.mapper.MisfitDataPointMapper;
-import org.openmhealth.shim.misfit.mapper.MisfitPhysicalActivityDataPointMapper;
-import org.openmhealth.shim.misfit.mapper.MisfitSleepDurationDataPointMapper;
-import org.openmhealth.shim.misfit.mapper.MisfitStepCountDataPointMapper;
+import org.openmhealth.shim.misfit.mapper.*;
import org.slf4j.Logger;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpHeaders;
@@ -71,6 +68,7 @@ public class MisfitShim extends OAuth2Shim {
private MisfitPhysicalActivityDataPointMapper physicalActivityMapper = new MisfitPhysicalActivityDataPointMapper();
private MisfitSleepDurationDataPointMapper sleepDurationMapper = new MisfitSleepDurationDataPointMapper();
+ private MisfitSleepEpisodeDataPointMapper sleepEpisodeMapper = new MisfitSleepEpisodeDataPointMapper();
private MisfitStepCountDataPointMapper stepCountMapper = new MisfitStepCountDataPointMapper();
@Override
@@ -121,6 +119,7 @@ public enum MisfitDataTypes implements ShimDataType {
PHYSICAL_ACTIVITY("activity/sessions"),
SLEEP_DURATION("activity/sleeps"),
+ SLEEP_EPISODE("activity/sleeps"),
STEP_COUNT("activity/summary");
private String endPoint;
@@ -196,6 +195,9 @@ protected ResponseEntity getData(OAuth2RestOperations restTemp
case SLEEP_DURATION:
dataPointMapper = sleepDurationMapper;
break;
+ case SLEEP_EPISODE:
+ dataPointMapper = sleepEpisodeMapper;
+ break;
case STEP_COUNT:
dataPointMapper = stepCountMapper;
break;
diff --git a/shim-server/src/main/java/org/openmhealth/shim/misfit/mapper/MisfitPhysicalActivityDataPointMapper.java b/shim-server/src/main/java/org/openmhealth/shim/misfit/mapper/MisfitPhysicalActivityDataPointMapper.java
index f1ff3730..4a67c2bc 100644
--- a/shim-server/src/main/java/org/openmhealth/shim/misfit/mapper/MisfitPhysicalActivityDataPointMapper.java
+++ b/shim-server/src/main/java/org/openmhealth/shim/misfit/mapper/MisfitPhysicalActivityDataPointMapper.java
@@ -35,7 +35,7 @@
*
* @author Emerson Farrugia
* @author Eric Jain
- * @see API documentation
+ * @see API documentation
*/
public class MisfitPhysicalActivityDataPointMapper extends MisfitDataPointMapper {
@@ -53,11 +53,8 @@ public Optional> asDataPoint(JsonNode sessionNode) {
PhysicalActivity.Builder builder = new PhysicalActivity.Builder(activityName);
- Optional distance = asOptionalDouble(sessionNode, "distance");
-
- if (distance.isPresent()) {
- builder.setDistance(new LengthUnitValue(MILE, distance.get()));
- }
+ asOptionalDouble(sessionNode, "distance")
+ .ifPresent(distanceInMi -> builder.setDistance(new LengthUnitValue(MILE, distanceInMi)));
Optional startDateTime = asOptionalOffsetDateTime(sessionNode, "startTime");
Optional durationInSec = asOptionalDouble(sessionNode, "duration");
diff --git a/shim-server/src/main/java/org/openmhealth/shim/misfit/mapper/MisfitSleepDurationDataPointMapper.java b/shim-server/src/main/java/org/openmhealth/shim/misfit/mapper/MisfitSleepDurationDataPointMapper.java
index 4d2f1d80..5aac493c 100644
--- a/shim-server/src/main/java/org/openmhealth/shim/misfit/mapper/MisfitSleepDurationDataPointMapper.java
+++ b/shim-server/src/main/java/org/openmhealth/shim/misfit/mapper/MisfitSleepDurationDataPointMapper.java
@@ -19,32 +19,31 @@
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.SleepDuration1;
-import org.openmhealth.shim.common.mapper.JsonNodeMappingException;
+import org.openmhealth.schema.domain.omh.SleepDuration2;
+import org.openmhealth.schema.domain.omh.SleepEpisode;
-import java.time.Duration;
import java.time.OffsetDateTime;
+import java.util.List;
import java.util.Optional;
-import static java.lang.String.format;
+import static java.util.Optional.empty;
import static org.openmhealth.schema.domain.omh.DurationUnit.SECOND;
import static org.openmhealth.schema.domain.omh.TimeInterval.ofStartDateTimeAndEndDateTime;
-import static org.openmhealth.shim.common.mapper.JsonNodeMappingSupport.*;
+import static org.openmhealth.shim.common.mapper.JsonNodeMappingSupport.asOptionalBoolean;
+import static org.openmhealth.shim.common.mapper.JsonNodeMappingSupport.asOptionalString;
+import static org.openmhealth.shim.misfit.mapper.MisfitSleepMeasureDataPointMapper.SleepSegmentType.AWAKE;
/**
- * A mapper from Misfit Resource API /activity/sleeps responses to {@link SleepDuration1} objects. This mapper
- * currently creates a single data point per sleep node in the response, subtracting the duration of awake segments
- * from the sleep duration. It's also possible to create a single data point per sleep segment, which would help
- * preserve the granularity of the original data. This mapper may be updated to return a data point per segment in the
- * future.
+ * A mapper from Misfit Resource API /activity/sleeps responses to {@link SleepDuration2} objects. This mapper currently
+ * creates a single data point per sleep node in the response, subtracting the duration of awake segments from the sleep
+ * duration. It's also possible to create a single data point per sleep segment, which would help preserve the
+ * granularity of the original data, but that may be better suited to {@link SleepEpisode} measures.
*
* @author Emerson Farrugia
- * @see API documentation
+ * @see API documentation
*/
-public class MisfitSleepDurationDataPointMapper extends MisfitDataPointMapper {
-
- public static final int AWAKE_SEGMENT_TYPE = 1;
+public class MisfitSleepDurationDataPointMapper extends MisfitSleepMeasureDataPointMapper {
@Override
protected String getListNodeName() {
@@ -52,55 +51,30 @@ protected String getListNodeName() {
}
@Override
- public Optional> asDataPoint(JsonNode sleepNode) {
-
- // The sleep details array contains segments corresponding to whether the user was awake, sleeping lightly,
- // or sleeping restfully for the duration of that segment. To discount the awake segments, we have to deduct
- // their duration from the total sleep duration.
- JsonNode sleepDetailsNode = asRequiredNode(sleepNode, "sleepDetails");
-
- long awakeDurationInSec = 0;
+ public Optional> asDataPoint(JsonNode sleepNode) {
- OffsetDateTime previousSegmentStartDateTime = null;
- Long previousSegmentType = null;
+ List sleepSegments = asSleepSegments(sleepNode);
- for (JsonNode sleepDetailSegmentNode : sleepDetailsNode) {
+ Optional effectiveStartDateTime = getSleepOnsetDateTime(sleepSegments);
- OffsetDateTime startDateTime = asRequiredOffsetDateTime(sleepDetailSegmentNode, "datetime");
- Long value = asRequiredLong(sleepDetailSegmentNode, "value");
-
- // if the user was awake, add it to the awake tally
- if (previousSegmentType != null && previousSegmentType == AWAKE_SEGMENT_TYPE) {
- awakeDurationInSec += Duration.between(previousSegmentStartDateTime, startDateTime).getSeconds();
- }
-
- previousSegmentStartDateTime = startDateTime;
- previousSegmentType = value;
+ if (!effectiveStartDateTime.isPresent()) {
+ return empty();
}
- // checking if the segment array is empty this way avoids compiler confusion later
- if (previousSegmentType == null) {
- throw new JsonNodeMappingException(format("The Misfit sleep node '%s' has no sleep details.", sleepNode));
- }
+ OffsetDateTime effectiveEndDateTime = getArisingDateTime(sleepSegments)
+ .orElseThrow(IllegalStateException::new);
- // to calculate the duration of last segment, first determine the overall end time
- OffsetDateTime startDateTime = asRequiredOffsetDateTime(sleepNode, "startTime");
- Long totalDurationInSec = asRequiredLong(sleepNode, "duration");
- OffsetDateTime endDateTime = startDateTime.plusSeconds(totalDurationInSec);
-
- if (previousSegmentType == AWAKE_SEGMENT_TYPE) {
- awakeDurationInSec += Duration.between(previousSegmentStartDateTime, endDateTime).getSeconds();
- }
-
- Long sleepDurationInSec = totalDurationInSec - awakeDurationInSec;
-
- if (sleepDurationInSec == 0) {
- return Optional.empty();
- }
+ long sleepDurationInSec = sleepSegments.stream()
+ .filter((segment) -> segment.getType() != AWAKE)
+ .mapToLong(SleepSegment::getDurationInSec)
+ .sum();
- SleepDuration1 measure = new SleepDuration1.Builder(new DurationUnitValue(SECOND, sleepDurationInSec))
- .setEffectiveTimeFrame(ofStartDateTimeAndEndDateTime(startDateTime, endDateTime))
- .build();
+ SleepDuration2 measure =
+ new SleepDuration2.Builder(
+ new DurationUnitValue(SECOND, sleepDurationInSec),
+ ofStartDateTimeAndEndDateTime(effectiveStartDateTime.get(), effectiveEndDateTime)
+ )
+ .build();
String externalId = asOptionalString(sleepNode, "id").orElse(null);
Boolean sensed = asOptionalBoolean(sleepNode, "autoDetected").orElse(null);
diff --git a/shim-server/src/main/java/org/openmhealth/shim/misfit/mapper/MisfitSleepEpisodeDataPointMapper.java b/shim-server/src/main/java/org/openmhealth/shim/misfit/mapper/MisfitSleepEpisodeDataPointMapper.java
new file mode 100644
index 00000000..1832c8b6
--- /dev/null
+++ b/shim-server/src/main/java/org/openmhealth/shim/misfit/mapper/MisfitSleepEpisodeDataPointMapper.java
@@ -0,0 +1,91 @@
+/*
+ * Copyright 2017 Open mHealth
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.openmhealth.shim.misfit.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.SleepEpisode;
+import org.openmhealth.schema.domain.omh.TimeInterval;
+
+import java.time.OffsetDateTime;
+import java.util.List;
+import java.util.Optional;
+
+import static java.util.Optional.empty;
+import static org.openmhealth.schema.domain.omh.DurationUnit.SECOND;
+import static org.openmhealth.schema.domain.omh.TimeInterval.ofStartDateTimeAndEndDateTime;
+import static org.openmhealth.shim.common.mapper.JsonNodeMappingSupport.asOptionalBoolean;
+import static org.openmhealth.shim.common.mapper.JsonNodeMappingSupport.asOptionalString;
+import static org.openmhealth.shim.misfit.mapper.MisfitSleepMeasureDataPointMapper.SleepSegmentType.AWAKE;
+
+
+/**
+ * A mapper from Misfit Resource API /activity/sleeps responses to {@link SleepEpisode} objects.
+ *
+ * @author Emerson Farrugia
+ * @see API documentation
+ */
+public class MisfitSleepEpisodeDataPointMapper extends MisfitSleepMeasureDataPointMapper {
+
+ @Override
+ protected String getListNodeName() {
+ return "sleeps";
+ }
+
+ @Override
+ public Optional> asDataPoint(JsonNode sleepNode) {
+
+ List sleepSegments = asSleepSegments(sleepNode);
+
+ Optional effectiveTimeInterval = getEffectiveTimeInterval(sleepSegments);
+
+ if (!effectiveTimeInterval.isPresent()) {
+ return empty();
+ }
+
+ SleepEpisode.Builder measureBuilder = new SleepEpisode.Builder(effectiveTimeInterval.get());
+
+ long sleepDurationInSec = sleepSegments.stream()
+ .filter((segment) -> segment.getType() != AWAKE)
+ .mapToLong(SleepSegment::getDurationInSec)
+ .sum();
+
+ measureBuilder.setTotalSleepTime(new DurationUnitValue(SECOND, sleepDurationInSec));
+
+ SleepEpisode measure = measureBuilder.build();
+
+ String externalId = asOptionalString(sleepNode, "id").orElse(null);
+ Boolean sensed = asOptionalBoolean(sleepNode, "autoDetected").orElse(null);
+
+ return Optional.of(newDataPoint(measure, RESOURCE_API_SOURCE_NAME, externalId, sensed));
+ }
+
+ private Optional getEffectiveTimeInterval(List sleepSegments) {
+
+ Optional effectiveStartDateTime = getSleepOnsetDateTime(sleepSegments);
+
+ if (!effectiveStartDateTime.isPresent()) {
+ return empty();
+ }
+
+ OffsetDateTime effectiveEndDateTime = getArisingDateTime(sleepSegments)
+ .orElseThrow(IllegalStateException::new);
+
+ return Optional.of(ofStartDateTimeAndEndDateTime(effectiveStartDateTime.get(), effectiveEndDateTime));
+ }
+}
diff --git a/shim-server/src/main/java/org/openmhealth/shim/misfit/mapper/MisfitSleepMeasureDataPointMapper.java b/shim-server/src/main/java/org/openmhealth/shim/misfit/mapper/MisfitSleepMeasureDataPointMapper.java
new file mode 100644
index 00000000..431e4116
--- /dev/null
+++ b/shim-server/src/main/java/org/openmhealth/shim/misfit/mapper/MisfitSleepMeasureDataPointMapper.java
@@ -0,0 +1,166 @@
+/*
+ * 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.misfit.mapper;
+
+import com.fasterxml.jackson.databind.JsonNode;
+import org.openmhealth.schema.domain.omh.SchemaSupport;
+import org.openmhealth.shim.common.mapper.JsonNodeMappingException;
+
+import java.time.Duration;
+import java.time.OffsetDateTime;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Optional;
+import java.util.stream.StreamSupport;
+
+import static java.lang.String.format;
+import static org.openmhealth.shim.common.mapper.JsonNodeMappingSupport.*;
+import static org.openmhealth.shim.misfit.mapper.MisfitSleepMeasureDataPointMapper.SleepSegmentType.AWAKE;
+
+
+/**
+ * @author Emerson Farrugia
+ */
+public abstract class MisfitSleepMeasureDataPointMapper extends MisfitDataPointMapper {
+
+ protected List asSleepSegments(JsonNode node) {
+
+ JsonNode sleepDetailsNode = asRequiredNode(node, "sleepDetails");
+
+ List sleepSegments = new ArrayList<>();
+ SleepSegment previousSegment = null;
+
+ for (JsonNode sleepDetailSegmentNode : sleepDetailsNode) {
+
+ SleepSegment sleepSegment = new SleepSegment();
+
+ sleepSegment.setStartDateTime(asRequiredOffsetDateTime(sleepDetailSegmentNode, "datetime"));
+ sleepSegment.setType(SleepSegmentType.getByMagicNumber(asRequiredInteger(sleepDetailSegmentNode, "value")));
+
+ sleepSegments.add(sleepSegment);
+
+ // finish constructing previous segment
+ if (previousSegment != null) {
+ previousSegment.setEndDateTime(sleepSegment.getStartDateTime());
+ }
+
+ previousSegment = sleepSegment;
+ }
+
+ // checking if the segment array is empty this way avoids compiler confusion later
+ if (previousSegment == null) {
+ throw new JsonNodeMappingException(format("The Misfit sleep node '%s' has no sleep details.", node));
+ }
+
+ // to calculate the end time of last segment, first determine the overall end time
+ OffsetDateTime startDateTime = asRequiredOffsetDateTime(node, "startTime");
+ Long totalDurationInSec = asRequiredLong(node, "duration");
+ OffsetDateTime endDateTime = startDateTime.plusSeconds(totalDurationInSec);
+
+ previousSegment.setEndDateTime(endDateTime);
+
+ return sleepSegments;
+ }
+
+
+ class SleepSegment {
+
+ private SleepSegmentType type;
+ private OffsetDateTime startDateTime;
+ private OffsetDateTime endDateTime;
+
+ public SleepSegmentType getType() {
+ return type;
+ }
+
+ public void setType(SleepSegmentType type) {
+ this.type = type;
+ }
+
+ public OffsetDateTime getStartDateTime() {
+ return startDateTime;
+ }
+
+ public void setStartDateTime(OffsetDateTime startDateTime) {
+ this.startDateTime = startDateTime;
+ }
+
+ public OffsetDateTime getEndDateTime() {
+ return endDateTime;
+ }
+
+ public void setEndDateTime(OffsetDateTime endDateTime) {
+ this.endDateTime = endDateTime;
+ }
+
+ public long getDurationInSec() {
+ return Duration.between(startDateTime, endDateTime).getSeconds();
+ }
+ }
+
+
+ enum SleepSegmentType {
+
+ AWAKE(1),
+ SLEEP(2),
+ DEEP_SLEEP(3);
+
+ private int magicNumber;
+
+ SleepSegmentType(int magicNumber) {
+ this.magicNumber = magicNumber;
+ }
+
+ /**
+ * @param magicNumber a magic number
+ * @return the constant corresponding to the magic number
+ */
+ public static SleepSegmentType getByMagicNumber(Integer magicNumber) {
+
+ for (SleepSegmentType constant : values()) {
+ if (constant.magicNumber == magicNumber) {
+ return constant;
+ }
+ }
+
+ throw new IllegalArgumentException(
+ format("A sleep segment type with value %d doesn't exist.", magicNumber));
+ }
+ }
+
+ /**
+ * @return the start time of the first non-awake entry, if any
+ */
+ protected Optional getSleepOnsetDateTime(List sleepSegments) {
+
+ return sleepSegments.stream()
+ .filter((segment) -> segment.getType() != AWAKE)
+ .map(SleepSegment::getStartDateTime)
+ .findFirst();
+ }
+
+ /**
+ * @return the end time of the last non-awake entry, if any
+ */
+ protected Optional getArisingDateTime(List sleepSegments) {
+
+ return sleepSegments.stream()
+ .filter((segment) -> segment.getType() != AWAKE)
+ .map(SleepSegment::getEndDateTime)
+ .reduce((first, second) -> second); // get last
+ }
+}
diff --git a/shim-server/src/main/java/org/openmhealth/shim/misfit/mapper/MisfitStepCountDataPointMapper.java b/shim-server/src/main/java/org/openmhealth/shim/misfit/mapper/MisfitStepCountDataPointMapper.java
index 11c3e5e1..7c6362cc 100644
--- a/shim-server/src/main/java/org/openmhealth/shim/misfit/mapper/MisfitStepCountDataPointMapper.java
+++ b/shim-server/src/main/java/org/openmhealth/shim/misfit/mapper/MisfitStepCountDataPointMapper.java
@@ -19,7 +19,8 @@
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.StepCount1;
+import org.openmhealth.schema.domain.omh.StepCount2;
+import org.openmhealth.schema.domain.omh.TimeInterval;
import java.time.LocalDate;
import java.time.OffsetDateTime;
@@ -27,6 +28,7 @@
import static com.google.common.base.Preconditions.checkNotNull;
import static java.time.ZoneOffset.UTC;
+import static java.util.Optional.empty;
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.asRequiredLocalDate;
@@ -34,13 +36,13 @@
/**
- * A mapper from Misfit Resource API /activity/summary?detail=true responses to {@link StepCount1} objects.
+ * A mapper from Misfit Resource API /activity/summary?detail=true responses to {@link StepCount2} objects.
*
* @author Emerson Farrugia
* @author Eric Jain
- * @see API documentation
+ * @see API documentation
*/
-public class MisfitStepCountDataPointMapper extends MisfitDataPointMapper {
+public class MisfitStepCountDataPointMapper extends MisfitDataPointMapper {
@Override
protected String getListNodeName() {
@@ -48,28 +50,27 @@ protected String getListNodeName() {
}
@Override
- public Optional> asDataPoint(JsonNode summaryNode) {
+ public Optional> asDataPoint(JsonNode summaryNode) {
checkNotNull(summaryNode);
Long stepCount = asRequiredLong(summaryNode, "steps");
if (stepCount == 0) {
- return Optional.empty();
+ return empty();
}
- StepCount1.Builder builder = new StepCount1.Builder(stepCount);
-
// this property isn't listed in the table, but does appear in the second Example section where detail is true
LocalDate localDate = asRequiredLocalDate(summaryNode, "date");
// FIXME fix the time zone offset once Misfit add it to the API
OffsetDateTime startDateTime = localDate.atStartOfDay().atOffset(UTC);
-
DurationUnitValue durationUnitValue = new DurationUnitValue(DAY, 1);
- builder.setEffectiveTimeFrame(ofStartDateTimeAndDuration(startDateTime, durationUnitValue));
- StepCount1 measure = builder.build();
+ TimeInterval effectiveTimeInterval = ofStartDateTimeAndDuration(startDateTime, durationUnitValue);
+
+ StepCount2 measure = new StepCount2.Builder(stepCount, effectiveTimeInterval)
+ .build();
return Optional.of(newDataPoint(measure, RESOURCE_API_SOURCE_NAME, null, null));
}
diff --git a/shim-server/src/main/java/org/openmhealth/shim/moves/Moves.md b/shim-server/src/main/java/org/openmhealth/shim/moves/Moves.md
index 05d780ad..e3a2c018 100644
--- a/shim-server/src/main/java/org/openmhealth/shim/moves/Moves.md
+++ b/shim-server/src/main/java/org/openmhealth/shim/moves/Moves.md
@@ -20,6 +20,7 @@ supports ETag header: yes
- protocol: OAuth 2.0
- https://dev.moves-app.com/docs/authentication
- authorization url: https://api.moves-app.com/oauth/v1/authorize?response_type=code&client_id=&scope=
+- also supports non-browser authorization for mobile apps
- supports refresh token: yes
# pagination
@@ -38,11 +39,49 @@ supports ETag header: yes
# endpoints
-steps
-- raw: summaries (/user/summary/daily)
+The storyline, activities, and summaries endpoints are similar. Storyline contains a superset of activities
+data (adds track points and locations), and activities contains a superset of summary data (adds segments).
+
+## get daily storyline
+
+- endpoint: /user/storyline/daily
+- required scopes: activity, location
+- reference: https://dev.moves-app.com/docs/api_storyline
+- limited to 7 day ranges
+
+# measures
+- calories-burned: not yet mapped
+- step-count: mapped
+- geo-position: not yet mapped
+- physical-activity: mapped
+
+## get daily activities
+
+- endpoint: /user/activities/daily
+- required scopes: activity
+- reference: https://dev.moves-app.com/docs/api_activities
+- probably limited to 31 days, not explicitly mentioned
+
+# measures
+- calories-burned: use storyline instead
+- step-count: use storyline instead
+- physical-activity: use storyline instead
+
+## get daily summaries
+
+- endpoint: /user/summary/daily
+- required scopes: activity
+- reference: https://dev.moves-app.com/docs/api_activities
+- limited to 31 days
+
+# measures
+- calories-burned: use storyline instead
+- step-count: use storyline instead
+- physical-activity: use storyline instead
+
+
+
-activity
-- raw: storyline (/user/storyline/daily)
# notifications
diff --git a/shim-server/src/main/java/org/openmhealth/shim/moves/MovesClientSettings.java b/shim-server/src/main/java/org/openmhealth/shim/moves/MovesClientSettings.java
index 5702f565..c3067db7 100644
--- a/shim-server/src/main/java/org/openmhealth/shim/moves/MovesClientSettings.java
+++ b/shim-server/src/main/java/org/openmhealth/shim/moves/MovesClientSettings.java
@@ -37,6 +37,8 @@ public class MovesClientSettings extends OAuth2ClientSettings {
"location"
);
+ private boolean authorizationInitiatedFromBrowser = true;
+
@Override
public List getScopes() {
@@ -47,4 +49,12 @@ public void setScopes(List scopes) {
this.scopes = scopes;
}
+
+ public boolean isAuthorizationInitiatedFromBrowser() {
+ return authorizationInitiatedFromBrowser;
+ }
+
+ public void setAuthorizationInitiatedFromBrowser(boolean authorizationInitiatedFromBrowser) {
+ this.authorizationInitiatedFromBrowser = authorizationInitiatedFromBrowser;
+ }
}
diff --git a/shim-server/src/main/java/org/openmhealth/shim/moves/MovesShim.java b/shim-server/src/main/java/org/openmhealth/shim/moves/MovesShim.java
index 220289f0..c68a6934 100644
--- a/shim-server/src/main/java/org/openmhealth/shim/moves/MovesShim.java
+++ b/shim-server/src/main/java/org/openmhealth/shim/moves/MovesShim.java
@@ -17,19 +17,16 @@
package org.openmhealth.shim.moves;
-import com.fasterxml.jackson.databind.JsonDeserializer;
import com.fasterxml.jackson.databind.JsonNode;
-import com.google.common.base.Splitter;
+import com.google.common.base.Joiner;
+import org.openmhealth.schema.domain.omh.DataPoint;
import org.openmhealth.shim.*;
-import org.openmhealth.shim.moves.mapper.MovesDataPointMapper;
import org.openmhealth.shim.moves.mapper.MovesPhysicalActivityDataPointMapper;
import org.openmhealth.shim.moves.mapper.MovesStepCountDataPointMapper;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
-import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
-import org.springframework.http.HttpMethod;
import org.springframework.http.ResponseEntity;
import org.springframework.security.oauth2.client.OAuth2RestOperations;
import org.springframework.security.oauth2.client.resource.OAuth2ProtectedResourceDetails;
@@ -39,16 +36,15 @@
import org.springframework.security.oauth2.client.token.grant.code.AuthorizationCodeAccessTokenProvider;
import org.springframework.stereotype.Component;
import org.springframework.util.MultiValueMap;
-import org.springframework.util.StringUtils;
import org.springframework.web.client.HttpClientErrorException;
import org.springframework.web.client.HttpServerErrorException;
import org.springframework.web.util.UriComponentsBuilder;
-import java.time.Duration;
-import java.time.OffsetDateTime;
+import java.time.LocalDate;
+import java.time.Period;
+import java.util.List;
import java.util.Map;
-import static java.util.Collections.singletonList;
import static org.springframework.http.ResponseEntity.ok;
@@ -63,34 +59,40 @@ public class MovesShim extends OAuth2Shim {
public static final String SHIM_KEY = "moves";
private static final String DATA_URL = "https://api.moves-app.com/api/1.1";
- private static final String USER_AUTHORIZATION_URL = "https://api.moves-app.com/oauth/v1/authorize";
+ private static final String WEB_BASED_USER_AUTHORIZATION_URL = "https://api.moves-app.com/oauth/v1/authorize";
+ private static final String APP_BASED_USER_AUTHORIZATION_URL = "moves://app/authorize";
private static final String ACCESS_TOKEN_URL = "https://api.moves-app.com/oauth/v1/access_token";
- private static final long MAX_DURATION_IN_DAYS = 31;
-
@Autowired
private MovesClientSettings clientSettings;
- private MovesStepCountDataPointMapper stepCountMapper = new MovesStepCountDataPointMapper();
private MovesPhysicalActivityDataPointMapper physicalActivityMapper = new MovesPhysicalActivityDataPointMapper();
+ private MovesStepCountDataPointMapper stepCountMapper = new MovesStepCountDataPointMapper();
+
@Override
public String getLabel() {
+
return "Moves";
}
@Override
public String getShimKey() {
+
return SHIM_KEY;
}
@Override
public String getUserAuthorizationUrl() {
- return USER_AUTHORIZATION_URL;
+
+ return clientSettings.isAuthorizationInitiatedFromBrowser()
+ ? WEB_BASED_USER_AUTHORIZATION_URL
+ : APP_BASED_USER_AUTHORIZATION_URL;
}
@Override
public String getAccessTokenUrl() {
+
return ACCESS_TOKEN_URL;
}
@@ -102,31 +104,45 @@ protected OAuth2ClientSettings getClientSettings() {
public enum MovesDataType implements ShimDataType {
- STEPS("/user/summary/daily"),
- ACTIVITY("/user/storyline/daily");
+ PHYSICAL_ACTIVITY("/user/storyline/daily"),
+ STEP_COUNT("/user/storyline/daily");
- private String endPoint;
+ private String endpoint;
+ private int maximumRetrievalPeriodInDays = 31;
- private JsonDeserializer normalizer;
+ MovesDataType(String endpoint) {
- MovesDataType(String endPoint) {
- this.endPoint = endPoint;
+ this.endpoint = endpoint;
+ }
+
+ MovesDataType(String endpoint, int maximumRetrievalPeriodInDays) {
+
+ this.endpoint = endpoint;
+ this.maximumRetrievalPeriodInDays = maximumRetrievalPeriodInDays;
}
public String getEndPoint() {
- return endPoint;
+
+ return endpoint;
+ }
+
+ public int getMaximumRetrievalPeriodInDays() {
+
+ return maximumRetrievalPeriodInDays;
}
}
public AuthorizationCodeAccessTokenProvider getAuthorizationCodeAccessTokenProvider() {
+
AuthorizationCodeAccessTokenProvider provider = new AuthorizationCodeAccessTokenProvider();
- provider.setTokenRequestEnhancer(new MovesTokenRequestEnhancer());
+ provider.setTokenRequestEnhancer(new MovesAccessTokenRequestEnhancer());
return provider;
}
@Override
public ShimDataType[] getShimDataTypes() {
+
return MovesDataType.values();
}
@@ -147,63 +163,56 @@ protected ResponseEntity getData(
+ dataTypeKey + " in shimDataRequest, cannot retrieve data.");
}
- OffsetDateTime now = OffsetDateTime.now();
-
- OffsetDateTime startDateTime = shimDataRequest.getStartDateTime() == null ?
- now.minusDays(1) : shimDataRequest.getStartDateTime();
-
- OffsetDateTime endDateTime = shimDataRequest.getEndDateTime() == null ?
- now.plusDays(1) : shimDataRequest.getEndDateTime();
+ LocalDate today = LocalDate.now();
- if (Duration.between(startDateTime, endDateTime).toDays() > MAX_DURATION_IN_DAYS) {
- endDateTime =
- startDateTime.plusDays(MAX_DURATION_IN_DAYS - 1); // TODO when refactoring, break apart queries
- }
+ LocalDate startDate = shimDataRequest.getStartDateTime() == null
+ ? today
+ : shimDataRequest.getStartDateTime().toLocalDate();
- UriComponentsBuilder uriBuilder = UriComponentsBuilder
- .fromUriString(DATA_URL);
+ LocalDate endDate = shimDataRequest.getEndDateTime() == null
+ ? today
+ : shimDataRequest.getEndDateTime().toLocalDate();
- for (String pathSegment : Splitter.on("/").split(movesDataType.getEndPoint())) {
- uriBuilder.pathSegment(pathSegment);
+ if (Period.between(startDate, endDate).getDays() > movesDataType.getMaximumRetrievalPeriodInDays()) {
+ endDate = startDate.plusDays(movesDataType.getMaximumRetrievalPeriodInDays());
}
- uriBuilder
- .queryParam("from", startDateTime.toLocalDate())
- .queryParam("to", endDateTime.toLocalDate())
- .queryParam("trackPoints", false);
-
- HttpHeaders headers = new HttpHeaders();
- headers.add("Authorization", "Bearer " + restTemplate.getAccessToken().getValue());
- HttpEntity entity = new HttpEntity("parameters", headers);
+ UriComponentsBuilder uriBuilder = UriComponentsBuilder.fromUriString(DATA_URL)
+ .path(movesDataType.getEndPoint())
+ .queryParam("from", startDate)
+ .queryParam("to", endDate)
+ .queryParam("trackPoints", false); // TODO make dynamic
ResponseEntity responseEntity;
+
try {
- responseEntity =
- restTemplate.exchange(uriBuilder.build().encode().toUri(), HttpMethod.GET, entity, JsonNode.class);
+ responseEntity = restTemplate.getForEntity(uriBuilder.build().encode().toUri(), JsonNode.class);
}
catch (HttpClientErrorException | HttpServerErrorException e) {
- // FIXME figure out how to handle this
+ // TODO figure out how to handle this
logger.error("A request for Moves data failed.", e);
throw e;
}
- if (shimDataRequest.getNormalize()) {
- MovesDataPointMapper> dataPointMapper;
+ List extends DataPoint>> dataPoints;
+
+ if (shimDataRequest.getNormalize()) {
switch (movesDataType) {
- case STEPS:
- dataPointMapper = stepCountMapper;
+ case PHYSICAL_ACTIVITY:
+ dataPoints = physicalActivityMapper.asDataPoints(responseEntity.getBody());
break;
- case ACTIVITY:
- dataPointMapper = physicalActivityMapper;
+
+ case STEP_COUNT:
+ dataPoints = stepCountMapper.asDataPoints(responseEntity.getBody());
break;
+
default:
throw new UnsupportedOperationException();
}
- return ok().body(ShimDataResponse.result(SHIM_KEY,
- dataPointMapper.asDataPoints(singletonList(responseEntity.getBody()))));
+ return ok().body(ShimDataResponse.result(SHIM_KEY, dataPoints));
}
else {
return ok().body(ShimDataResponse.result(SHIM_KEY, responseEntity.getBody()));
@@ -211,32 +220,31 @@ protected ResponseEntity getData(
}
@Override
- protected String getAuthorizationUrl(UserRedirectRequiredException exception, Map addlParameters) {
+ protected String getAuthorizationUrl(
+ UserRedirectRequiredException exception,
+ Map additionalParameters) {
final OAuth2ProtectedResourceDetails resource = getResource();
UriComponentsBuilder uriBuilder = UriComponentsBuilder
.fromUriString(exception.getRedirectUri())
- .queryParam("state", exception.getStateKey())
- .queryParam("client_id", resource.getClientId())
.queryParam("response_type", "code")
- .queryParam("access_type", "offline")
- .queryParam("approval_prompt", "force")
- .queryParam("scope", StringUtils.collectionToDelimitedString(resource.getScope(), " "))
- .queryParam("redirect_uri", getDefaultRedirectUrl());
+ .queryParam("client_id", resource.getClientId())
+ .queryParam("redirect_uri", getDefaultRedirectUrl())
+ .queryParam("scope", Joiner.on(" ").join(resource.getScope()))
+ .queryParam("state", exception.getStateKey());
return uriBuilder.build().encode().toUriString();
}
- /**
- * Adds required parameters to authorization token requests.
- */
- private class MovesTokenRequestEnhancer implements RequestEnhancer {
+ private class MovesAccessTokenRequestEnhancer implements RequestEnhancer {
@Override
- public void enhance(AccessTokenRequest request,
+ public void enhance(
+ AccessTokenRequest request,
OAuth2ProtectedResourceDetails resource,
- MultiValueMap form, HttpHeaders headers) {
+ MultiValueMap form,
+ HttpHeaders headers) {
form.set("client_id", resource.getClientId());
form.set("client_secret", resource.getClientSecret());
diff --git a/shim-server/src/main/java/org/openmhealth/shim/moves/mapper/MovesActivityNodeDataPointMapper.java b/shim-server/src/main/java/org/openmhealth/shim/moves/mapper/MovesActivityNodeDataPointMapper.java
new file mode 100644
index 00000000..561f0717
--- /dev/null
+++ b/shim-server/src/main/java/org/openmhealth/shim/moves/mapper/MovesActivityNodeDataPointMapper.java
@@ -0,0 +1,93 @@
+/*
+ * Copyright 2017 Open mHealth
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.openmhealth.shim.moves.mapper;
+
+
+import com.fasterxml.jackson.databind.JsonNode;
+import org.openmhealth.schema.domain.omh.DataPoint;
+import org.openmhealth.schema.domain.omh.SchemaSupport;
+import org.openmhealth.schema.domain.omh.TimeFrame;
+import org.openmhealth.shim.OptionalStreamSupport;
+
+import java.util.List;
+import java.util.Optional;
+import java.util.stream.Collectors;
+import java.util.stream.StreamSupport;
+
+import static com.google.common.base.Preconditions.checkArgument;
+import static com.google.common.base.Preconditions.checkNotNull;
+import static org.openmhealth.shim.OptionalStreamSupport.asStream;
+import static org.openmhealth.shim.common.mapper.JsonNodeMappingSupport.asOptionalNode;
+import static org.openmhealth.shim.common.mapper.JsonNodeMappingSupport.asRequiredString;
+
+
+/**
+ * A mapper from activity nodes in Moves Resource API /user/storyline/daily responses to data points.
+ *
+ * @author Emerson Farrugia
+ * @author Jared Sieling
+ * @see API documentation
+ */
+public abstract class MovesActivityNodeDataPointMapper extends MovesDataPointMapper {
+
+ @Override
+ public List> asDataPoints(List responseNodes) {
+
+ checkNotNull(responseNodes);
+ checkArgument(responseNodes.size() == 1, "A single response node is allowed per call.");
+
+ return StreamSupport.stream(responseNodes.get(0).spliterator(), false)
+ .flatMap(dayNode -> asStream(asOptionalNode(dayNode, "segments")))
+ .flatMap(segmentsNode -> StreamSupport.stream(segmentsNode.spliterator(), false))
+ .flatMap(segmentNode -> asStream(asOptionalNode(segmentNode, "activities")))
+ .flatMap(activitiesNode -> StreamSupport.stream(activitiesNode.spliterator(), false))
+ .map(this::asDataPoint)
+ .flatMap(OptionalStreamSupport::asStream)
+ .collect(Collectors.toList());
+ }
+
+ /**
+ * Creates a data point.
+ *
+ * @param node a node containing all the information required to build the data point
+ * @return a data point
+ */
+ protected Optional> asDataPoint(JsonNode node) {
+
+ Optional measure = newMeasure(node);
+
+ return measure.map(m -> asDataPoint(node, m, newExternalId(node)));
+ }
+
+ /**
+ * @param node a node containing all the information required to build the measure
+ * @return a measure
+ */
+ protected abstract Optional newMeasure(JsonNode node);
+
+ /**
+ * @param node a node containing all the information required to construct a unique identifier
+ * @return a unique identifier for this node
+ */
+ private String newExternalId(JsonNode node) {
+
+ String qualifier = asRequiredString(node, "activity");
+ TimeFrame timeFrame = getTimeFrame(node).orElseThrow(IllegalStateException::new);
+
+ return String.format("%s-%d", qualifier, timeFrame.getTimeInterval().getStartDateTime().toEpochSecond());
+ }
+}
diff --git a/shim-server/src/main/java/org/openmhealth/shim/moves/mapper/MovesDataPointMapper.java b/shim-server/src/main/java/org/openmhealth/shim/moves/mapper/MovesDataPointMapper.java
index eabbd5ee..3f0801fd 100644
--- a/shim-server/src/main/java/org/openmhealth/shim/moves/mapper/MovesDataPointMapper.java
+++ b/shim-server/src/main/java/org/openmhealth/shim/moves/mapper/MovesDataPointMapper.java
@@ -1,104 +1,102 @@
+/*
+ * Copyright 2017 Open mHealth
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
package org.openmhealth.shim.moves.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.JsonNodeDataPointMapper;
-import javax.annotation.Nullable;
-import java.time.LocalDateTime;
import java.time.OffsetDateTime;
-import java.time.ZoneOffset;
-import java.util.List;
+import java.time.format.DateTimeFormatter;
import java.util.Optional;
-import java.util.UUID;
-import static com.google.common.base.Preconditions.checkArgument;
-import static com.google.common.base.Preconditions.checkNotNull;
-import static org.openmhealth.shim.common.mapper.JsonNodeMappingSupport.asOptionalLocalDateTime;
+import static java.util.Optional.empty;
+import static java.util.UUID.randomUUID;
+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.TimeInterval.ofStartDateTimeAndEndDateTime;
+import static org.openmhealth.shim.common.mapper.JsonNodeMappingSupport.asOptionalBoolean;
+import static org.openmhealth.shim.common.mapper.JsonNodeMappingSupport.asOptionalOffsetDateTime;
+
/**
* The base class for mappers that translate Moves API responses to data points.
*
- * @author Jared Sieling.
+ * @author Jared Sieling
+ * @author Emerson Farrugia
*/
public abstract class MovesDataPointMapper implements JsonNodeDataPointMapper {
public static final String RESOURCE_API_SOURCE_NAME = "Moves Resource API";
- @Override
- public List> asDataPoints(List responseNodes) {
+ protected static final DateTimeFormatter OFFSET_DATE_TIME_FORMATTER =
+ DateTimeFormatter.ofPattern("yyyyMMdd'T'HHmmssx");
- checkNotNull(responseNodes);
- checkArgument(responseNodes.size() == 1, "A single response node is allowed per call.");
- JsonNode listNode = responseNodes.get(0);
+ /**
+ * @param node a node containing optional "startTime" and "endTime" fields
+ * @return the equivalent time frame, if any
+ */
+ public Optional getTimeFrame(JsonNode node) {
+
+ Optional startDateTime =
+ asOptionalOffsetDateTime(node, "startTime", OFFSET_DATE_TIME_FORMATTER);
- List> dataPoints = Lists.newArrayList();
+ Optional endDateTime = asOptionalOffsetDateTime(node, "endTime", OFFSET_DATE_TIME_FORMATTER);
- for (JsonNode listEntryNode : listNode) {
- asDataPoint(listEntryNode).ifPresent(dataPoints::add);
+ if (!startDateTime.isPresent() || !endDateTime.isPresent()) {
+ return empty();
}
- return dataPoints;
+ return Optional.of(new TimeFrame(ofStartDateTimeAndEndDateTime(startDateTime.get(), endDateTime.get())));
}
- /**
- * Creates a data point.
- *
- * @param measure the measure to set as the body of the data point
- * @param externalId the identifier of the measure as recorded by the data provider
- * @param the measure type
- * @return a data point
+ /**
+ * @param node a node containing an optional "manual" field
+ * @return the equivalent modality, if any
*/
- protected DataPoint newDataPoint(T measure, @Nullable Long externalId) {
-
- DataPointAcquisitionProvenance acquisitionProvenance =
- new DataPointAcquisitionProvenance.Builder(RESOURCE_API_SOURCE_NAME).build();
-
- if (externalId != null) {
- acquisitionProvenance.setAdditionalProperty("external_id", externalId);
- }
+ public Optional getModality(JsonNode node) {
- DataPointHeader header = new DataPointHeader.Builder(UUID.randomUUID().toString(), measure.getSchemaId())
- .setAcquisitionProvenance(acquisitionProvenance).build();
-
- return new DataPoint<>(header, measure);
+ return asOptionalBoolean(node, "manual")
+ .map(manual -> manual ? SELF_REPORTED : SENSED);
}
/**
- * FIXME this is copy-pasted from Fitbit, and it's not clear why this would apply
- * @param node a JSON node containing date and time properties
- * @return the equivalent OffsetDateTime
+ * Creates a data point.
+ *
+ * @param node a node containing all the information required to build the data point
+ * @return a data point
*/
- protected Optional combineDateTimeAndTimezone(JsonNode node) {
+ protected DataPoint asDataPoint(JsonNode node, T measure, String externalId) {
- Optional dateTime = asOptionalLocalDateTime(node, "date", "time");
- Optional offsetDateTime = null;
+ DataPointAcquisitionProvenance.Builder acquisitionProvenanceBuilder =
+ new DataPointAcquisitionProvenance.Builder(RESOURCE_API_SOURCE_NAME);
- if (dateTime.isPresent()) {
- offsetDateTime = Optional.of(OffsetDateTime.of(dateTime.get(), ZoneOffset.UTC));
- }
+ Optional modality = getModality(node);
+ modality.ifPresent(acquisitionProvenanceBuilder::setModality);
- return offsetDateTime;
- }
+ DataPointAcquisitionProvenance acquisitionProvenance = acquisitionProvenanceBuilder.build();
- /**
- * FIXME this is copy-pasted from Fitbit, and it's not clear why this would apply
- * Transforms a {@link LocalDateTime} object into an {@link OffsetDateTime} object with a UTC time zone
- *
- * @param dateTime local date and time for the Moves response JSON node
- * @return the date and time based on the input dateTime parameter
- */
- protected OffsetDateTime combineDateTimeAndTimezone(LocalDateTime dateTime) {
+ acquisitionProvenance.setAdditionalProperty("external_id", externalId);
- return OffsetDateTime.of(dateTime, ZoneOffset.UTC);
- }
+ DataPointHeader header = new DataPointHeader.Builder(randomUUID().toString(), measure.getSchemaId())
+ .setAcquisitionProvenance(acquisitionProvenance)
+ .build();
- /**
- * Maps a JSON response node from the Moves API into a data point.
- *
- * @return the data point
- */
- protected abstract Optional> asDataPoint(JsonNode node);
+ return new DataPoint<>(header, measure);
+ }
}
diff --git a/shim-server/src/main/java/org/openmhealth/shim/moves/mapper/MovesPhysicalActivityDataPointMapper.java b/shim-server/src/main/java/org/openmhealth/shim/moves/mapper/MovesPhysicalActivityDataPointMapper.java
index c6ff3593..d1943642 100644
--- a/shim-server/src/main/java/org/openmhealth/shim/moves/mapper/MovesPhysicalActivityDataPointMapper.java
+++ b/shim-server/src/main/java/org/openmhealth/shim/moves/mapper/MovesPhysicalActivityDataPointMapper.java
@@ -1,90 +1,66 @@
+/*
+ * Copyright 2017 Open mHealth
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
package org.openmhealth.shim.moves.mapper;
import com.fasterxml.jackson.databind.JsonNode;
-import com.google.common.collect.Lists;
-import org.openmhealth.schema.domain.omh.DataPoint;
-import org.openmhealth.schema.domain.omh.DurationUnitValue;
+import org.openmhealth.schema.domain.omh.KcalUnitValue;
import org.openmhealth.schema.domain.omh.LengthUnitValue;
import org.openmhealth.schema.domain.omh.PhysicalActivity;
+import org.openmhealth.schema.domain.omh.TimeFrame;
-import java.time.OffsetDateTime;
-import java.time.format.DateTimeFormatter;
-import java.util.List;
import java.util.Optional;
-import static com.google.common.base.Preconditions.checkArgument;
-import static com.google.common.base.Preconditions.checkNotNull;
-import static org.openmhealth.schema.domain.omh.DurationUnit.SECOND;
+import static java.util.Optional.empty;
+import static org.openmhealth.schema.domain.omh.KcalUnit.KILOCALORIE;
import static org.openmhealth.schema.domain.omh.LengthUnit.METER;
-import static org.openmhealth.schema.domain.omh.TimeInterval.ofStartDateTimeAndDuration;
-import static org.openmhealth.shim.common.mapper.JsonNodeMappingSupport.*;
+import static org.openmhealth.shim.common.mapper.JsonNodeMappingSupport.asOptionalDouble;
+import static org.openmhealth.shim.common.mapper.JsonNodeMappingSupport.asRequiredString;
+
/**
- * A mapper that translates responses from the Moves Resource API /user/storyline/daily endpoint into {@link
- * PhysicalActivity} data points.
+ * A mapper that translates responses from the Moves Resource API /user/storyline/daily endpoint into
+ * {@link PhysicalActivity} data points.
*
+ * @author Emerson Farrugia
* @author Jared Sieling
* @see API documentation
*/
-public class MovesPhysicalActivityDataPointMapper extends MovesDataPointMapper{
+public class MovesPhysicalActivityDataPointMapper extends MovesActivityNodeDataPointMapper {
- /**
- * Override because the the day-to-dataPoint relationship isn't one-to-one.
- */
@Override
- public List> asDataPoints(List responseNodes) {
-
- checkNotNull(responseNodes);
- checkArgument(responseNodes.size() == 1, "A single response node is allowed per call.");
-
- JsonNode listNode = responseNodes.get(0);
-
- List> dataPoints = Lists.newArrayList();
-
- for (JsonNode listEntryNode : listNode) {
- JsonNode segments = asRequiredNode(listEntryNode, "segments");
+ protected Optional newMeasure(JsonNode node) {
- // Filter out segments that are of type 'place' or activity 'transport'.
- for (JsonNode segment : segments) {
- if(segment.get("type").asText().equals("move")) {
- JsonNode activities = asRequiredNode(segment, "activities");
+ Optional timeFrame = getTimeFrame(node);
- for (JsonNode activity : activities) {
- if(!activity.get("group").asText().equals("transport")) {
- asDataPoint(activity).ifPresent(dataPoints::add);
- }
- }
- }
- }
+ if (!timeFrame.isPresent()) {
+ return empty();
}
- return dataPoints;
- }
-
- @Override
- protected Optional> asDataPoint(JsonNode node) {
-
String activityName = asRequiredString(node, "activity");
- PhysicalActivity.Builder builder = new PhysicalActivity.Builder(activityName);
- Optional distance = asOptionalDouble(node, "distance");
-
- distance.ifPresent(aDouble -> builder.setDistance(new LengthUnitValue(METER, aDouble)));
-
- // TODO update JSON utilities
- DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyyMMdd'T'HHmmssx");
- OffsetDateTime dateTime = OffsetDateTime.parse(node.get("startTime").asText(), formatter);
- Optional startDateTime = Optional.ofNullable(dateTime);
-
- Optional durationInSec = asOptionalDouble(node, "duration");
+ PhysicalActivity.Builder builder = new PhysicalActivity.Builder(activityName);
+ builder.setEffectiveTimeFrame(timeFrame.get());
- if (startDateTime.isPresent() && durationInSec.isPresent()) {
- DurationUnitValue durationUnitValue = new DurationUnitValue(SECOND, durationInSec.get());
- builder.setEffectiveTimeFrame(ofStartDateTimeAndDuration(startDateTime.get(), durationUnitValue));
- }
+ asOptionalDouble(node, "distance")
+ .ifPresent(distanceInM -> builder.setDistance(new LengthUnitValue(METER, distanceInM)));
- PhysicalActivity measure = builder.build();
+ asOptionalDouble(node, "calories")
+ .ifPresent(calories -> builder.setCaloriesBurned(new KcalUnitValue(KILOCALORIE, calories)));
- return Optional.of(newDataPoint(measure, null));
+ return Optional.of(builder.build());
}
}
diff --git a/shim-server/src/main/java/org/openmhealth/shim/moves/mapper/MovesSegmentNodeDataPointMapper.java b/shim-server/src/main/java/org/openmhealth/shim/moves/mapper/MovesSegmentNodeDataPointMapper.java
new file mode 100644
index 00000000..9345f4bf
--- /dev/null
+++ b/shim-server/src/main/java/org/openmhealth/shim/moves/mapper/MovesSegmentNodeDataPointMapper.java
@@ -0,0 +1,91 @@
+/*
+ * Copyright 2017 Open mHealth
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.openmhealth.shim.moves.mapper;
+
+
+import com.fasterxml.jackson.databind.JsonNode;
+import org.openmhealth.schema.domain.omh.DataPoint;
+import org.openmhealth.schema.domain.omh.SchemaSupport;
+import org.openmhealth.schema.domain.omh.TimeFrame;
+import org.openmhealth.shim.OptionalStreamSupport;
+
+import java.util.List;
+import java.util.Optional;
+import java.util.stream.Collectors;
+import java.util.stream.StreamSupport;
+
+import static com.google.common.base.Preconditions.checkArgument;
+import static com.google.common.base.Preconditions.checkNotNull;
+import static org.openmhealth.shim.OptionalStreamSupport.asStream;
+import static org.openmhealth.shim.common.mapper.JsonNodeMappingSupport.asOptionalNode;
+import static org.openmhealth.shim.common.mapper.JsonNodeMappingSupport.asRequiredString;
+
+
+/**
+ * A mapper from segment nodes in Moves Resource API /user/storyline/daily responses to data points.
+ *
+ * @author Emerson Farrugia
+ * @author Jared Sieling
+ * @see API documentation
+ */
+public abstract class MovesSegmentNodeDataPointMapper extends MovesDataPointMapper {
+
+ @Override
+ public List> asDataPoints(List responseNodes) {
+
+ checkNotNull(responseNodes);
+ checkArgument(responseNodes.size() == 1, "A single response node is allowed per call.");
+
+ return StreamSupport.stream(responseNodes.get(0).spliterator(), false)
+ .flatMap(dayNode -> asStream(asOptionalNode(dayNode, "segments")))
+ .flatMap(segmentsNode -> StreamSupport.stream(segmentsNode.spliterator(), false))
+ .map(this::asDataPoint)
+ .flatMap(OptionalStreamSupport::asStream)
+ .collect(Collectors.toList());
+ }
+
+ /**
+ * Creates a data point.
+ *
+ * @param node a node containing all the information required to build the data point
+ * @return a data point
+ */
+ protected Optional> asDataPoint(JsonNode node) {
+
+ Optional measure = newMeasure(node);
+
+ return measure.map(m -> asDataPoint(node, m, newExternalId(node)));
+ }
+
+ /**
+ * @param node a node containing all the information required to build the measure
+ * @return a measure
+ */
+ protected abstract Optional newMeasure(JsonNode node);
+
+ /**
+ * @param node a node containing all the information required to construct a unique identifier
+ * @return a unique identifier for this node
+ */
+ private String newExternalId(JsonNode node) {
+
+ String qualifier = asRequiredString(node, "type");
+ TimeFrame timeFrame = getTimeFrame(node).orElseThrow(IllegalStateException::new);
+
+ return String.format("%s-%d", qualifier, timeFrame.getTimeInterval().getStartDateTime().toEpochSecond());
+ }
+}
\ No newline at end of file
diff --git a/shim-server/src/main/java/org/openmhealth/shim/moves/mapper/MovesStepCountDataPointMapper.java b/shim-server/src/main/java/org/openmhealth/shim/moves/mapper/MovesStepCountDataPointMapper.java
index bc9262fe..7c7bcf46 100644
--- a/shim-server/src/main/java/org/openmhealth/shim/moves/mapper/MovesStepCountDataPointMapper.java
+++ b/shim-server/src/main/java/org/openmhealth/shim/moves/mapper/MovesStepCountDataPointMapper.java
@@ -1,58 +1,52 @@
+/*
+ * Copyright 2017 Open mHealth
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
package org.openmhealth.shim.moves.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.StepCount1;
-import org.openmhealth.schema.domain.omh.TimeInterval;
-
-import java.time.LocalDate;
-import java.time.LocalDateTime;
-import java.time.format.DateTimeFormatter;
+import org.openmhealth.schema.domain.omh.StepCount2;
+import org.openmhealth.schema.domain.omh.TimeFrame;
+
import java.util.Optional;
-import static org.openmhealth.schema.domain.omh.DurationUnit.DAY;
-import static org.openmhealth.shim.common.mapper.JsonNodeMappingSupport.asOptionalLocalDate;
+import static java.util.Optional.empty;
+import static org.openmhealth.shim.common.mapper.JsonNodeMappingSupport.asOptionalLong;
+
/**
- * A mapper from Moves Resource API /user/summary/daily responses to {@link StepCount1} objects.
+ * A mapper that translates responses from the Moves Resource API /user/storyline/daily endpoint into
+ * {@link StepCount2} data points.
*
- * @author Jared Sieling
- * @see API documentation
+ * @author Emerson Farrugia
+ * @see API documentation
*/
-public class MovesStepCountDataPointMapper extends MovesDataPointMapper{
+public class MovesStepCountDataPointMapper extends MovesActivityNodeDataPointMapper {
@Override
- protected Optional> asDataPoint(JsonNode node) {
-
- // Sum steps from all individual activities
- int stepCountValue = 0;
- for(JsonNode activity : node.get("summary")){
- if(activity.has("steps")){
- stepCountValue = stepCountValue + activity.get("steps").asInt();
- }
- }
-
- if (stepCountValue == 0) {
- return Optional.empty();
- }
+ protected Optional newMeasure(JsonNode node) {
- StepCount1.Builder builder = new StepCount1.Builder(stepCountValue);
+ Optional timeFrame = getTimeFrame(node);
- Optional stepDate = asOptionalLocalDate(node, "date", DateTimeFormatter.BASIC_ISO_DATE);
-
- if (stepDate.isPresent()) {
- LocalDateTime startDateTime = stepDate.get().atTime(0, 0, 0, 0);
-
- // FIXME the time zone handling here is suspect; if the code is going to assume UTC, the shim should be
- // asking for UTC in the initial request
- builder.setEffectiveTimeFrame(
- TimeInterval.ofStartDateTimeAndDuration(combineDateTimeAndTimezone(startDateTime),
- new DurationUnitValue(DAY, 1)));
+ // a time frame seems to not be present for manually entered data, making it impossible to deduplicate
+ if (!timeFrame.isPresent()) {
+ return empty();
}
- StepCount1 measure = builder.build();
-
- return Optional.of(newDataPoint(measure, null));
+ return asOptionalLong(node, "steps")
+ .filter(count -> count > 0)
+ .map(count -> new StepCount2.Builder(count, timeFrame.get()).build());
}
}
diff --git a/shim-server/src/main/java/org/openmhealth/shim/runkeeper/mapper/RunkeeperCaloriesBurnedDataPointMapper.java b/shim-server/src/main/java/org/openmhealth/shim/runkeeper/mapper/RunkeeperCaloriesBurnedDataPointMapper.java
index 887db860..11e5b466 100644
--- a/shim-server/src/main/java/org/openmhealth/shim/runkeeper/mapper/RunkeeperCaloriesBurnedDataPointMapper.java
+++ b/shim-server/src/main/java/org/openmhealth/shim/runkeeper/mapper/RunkeeperCaloriesBurnedDataPointMapper.java
@@ -17,57 +17,48 @@
package org.openmhealth.shim.runkeeper.mapper;
import com.fasterxml.jackson.databind.JsonNode;
-import org.openmhealth.schema.domain.omh.CaloriesBurned;
+import org.openmhealth.schema.domain.omh.CaloriesBurned2;
import org.openmhealth.schema.domain.omh.DataPoint;
-import org.openmhealth.schema.domain.omh.KcalUnit;
-import org.openmhealth.schema.domain.omh.KcalUnitValue;
import java.util.Optional;
+import static org.openmhealth.schema.domain.omh.KcalUnit.KILOCALORIE;
import static org.openmhealth.shim.common.mapper.JsonNodeMappingSupport.asOptionalDouble;
import static org.openmhealth.shim.common.mapper.JsonNodeMappingSupport.asOptionalString;
/**
* A mapper from RunKeeper HealthGraph API application/vnd.com.runkeeper.FitnessActivityFeed+json responses to {@link
- * CaloriesBurned} objects.
+ * CaloriesBurned2} objects.
*
* @author Chris Schaefbauer
* @author Emerson Farrugia
*/
-public class RunkeeperCaloriesBurnedDataPointMapper extends RunkeeperDataPointMapper {
-
+public class RunkeeperCaloriesBurnedDataPointMapper extends RunkeeperDataPointMapper {
@Override
- protected Optional> asDataPoint(JsonNode itemNode) {
-
- Optional caloriesBurned = getMeasure(itemNode);
+ protected Optional> asDataPoint(JsonNode itemNode) {
- if (caloriesBurned.isPresent()) {
- return Optional
- .of(new DataPoint<>(getDataPointHeader(itemNode, caloriesBurned.get()), caloriesBurned.get()));
- }
- else {
- return Optional.empty(); // return empty if there was no calories information to generate a datapoint
- }
+ Optional caloriesBurned = newMeasure(itemNode);
+ // return empty if there was no calories information to generate a data point
+ return caloriesBurned
+ .map(measure -> new DataPoint<>(getDataPointHeader(itemNode, measure), measure));
}
- private Optional getMeasure(JsonNode itemNode) {
+ private Optional newMeasure(JsonNode itemNode) {
Optional calorieValue = asOptionalDouble(itemNode, "total_calories");
- if (!calorieValue.isPresent()) { // Not all activity datapoints have the "total_calories" property
+
+ if (!calorieValue.isPresent()) { // not all activity data points have the "total_calories" property
return Optional.empty();
}
- CaloriesBurned.Builder caloriesBurnedBuilder =
- new CaloriesBurned.Builder(new KcalUnitValue(KcalUnit.KILOCALORIE, calorieValue.get()));
- setEffectiveTimeFrameIfPresent(itemNode, caloriesBurnedBuilder);
+ CaloriesBurned2.Builder caloriesBurnedBuilder =
+ new CaloriesBurned2.Builder(KILOCALORIE.newUnitValue(calorieValue.get()), getTimeFrame(itemNode));
- Optional activityType = asOptionalString(itemNode, "type");
- activityType.ifPresent(at -> caloriesBurnedBuilder.setActivityName(at));
+ asOptionalString(itemNode, "type").ifPresent(caloriesBurnedBuilder::setActivityName);
return Optional.of(caloriesBurnedBuilder.build());
-
}
}
diff --git a/shim-server/src/main/java/org/openmhealth/shim/runkeeper/mapper/RunkeeperDataPointMapper.java b/shim-server/src/main/java/org/openmhealth/shim/runkeeper/mapper/RunkeeperDataPointMapper.java
index 1ee638dd..31a54554 100644
--- a/shim-server/src/main/java/org/openmhealth/shim/runkeeper/mapper/RunkeeperDataPointMapper.java
+++ b/shim-server/src/main/java/org/openmhealth/shim/runkeeper/mapper/RunkeeperDataPointMapper.java
@@ -102,7 +102,6 @@ protected DataPointHeader getDataPointHeader(JsonNode itemNode, Measure measure)
asOptionalInteger(itemNode, "userId").ifPresent(userId -> headerBuilder.setUserId(userId.toString()));
return headerBuilder.build();
-
}
/**
@@ -129,31 +128,47 @@ public Optional getModality(JsonNode itemNode) {
}
return Optional.empty();
-
}
/**
- * Sets the effective time frame property for a measure builder.
- *
- * @param itemNode an individual datapoint from the list of datapoints returned in the API response
- * @param builder the measure builder to have the effective date property set
+ * @param node a JSON node optionally containing time frame properties
*/
- protected void setEffectiveTimeFrameIfPresent(JsonNode itemNode, Measure.Builder builder) {
+ protected Optional getOptionalTimeFrame(JsonNode node) {
Optional localStartDateTime =
- asOptionalLocalDateTime(itemNode, "start_time", DATE_TIME_FORMATTER);
+ asOptionalLocalDateTime(node, "start_time", DATE_TIME_FORMATTER);
// RunKeeper doesn't support fractional time zones
- Optional utcOffset = asOptionalInteger(itemNode, "utc_offset");
- Optional durationInS = asOptionalDouble(itemNode, "duration");
+ Optional utcOffset = asOptionalInteger(node, "utc_offset");
+ Optional durationInSeconds = asOptionalDouble(node, "duration");
+
+ if (!localStartDateTime.isPresent() || !utcOffset.isPresent() || !durationInSeconds.isPresent()) {
+ return Optional.empty();
+ }
- if (localStartDateTime.isPresent() && utcOffset.isPresent() && durationInS.isPresent()) {
+ return Optional.of(asTimeFrame(localStartDateTime.get(), utcOffset.get(), durationInSeconds.get()));
+ }
- OffsetDateTime startDateTime = localStartDateTime.get().atOffset(ZoneOffset.ofHours(utcOffset.get()));
- DurationUnitValue duration = new DurationUnitValue(SECOND, durationInS.get());
+ private TimeFrame asTimeFrame(LocalDateTime localStartDateTime, int utcOffsetInHours, Double durationInSeconds) {
- builder.setEffectiveTimeFrame(ofStartDateTimeAndDuration(startDateTime, duration));
- }
+ OffsetDateTime startDateTime = localStartDateTime.atOffset(ZoneOffset.ofHours(utcOffsetInHours));
+
+ return new TimeFrame(ofStartDateTimeAndDuration(startDateTime, SECOND.newUnitValue(durationInSeconds)));
+ }
+
+ /**
+ * @param node a JSON node containing time frame properties
+ */
+ protected TimeFrame getTimeFrame(JsonNode node) {
+
+ LocalDateTime localStartDateTime =
+ asRequiredLocalDateTime(node, "start_time", DATE_TIME_FORMATTER);
+
+ // RunKeeper doesn't support fractional time zones
+ Integer utcOffset = asRequiredInteger(node, "utc_offset");
+ Double durationInSeconds = asRequiredDouble(node, "duration");
+
+ return asTimeFrame(localStartDateTime, utcOffset, durationInSeconds);
}
/**
diff --git a/shim-server/src/main/java/org/openmhealth/shim/runkeeper/mapper/RunkeeperPhysicalActivityDataPointMapper.java b/shim-server/src/main/java/org/openmhealth/shim/runkeeper/mapper/RunkeeperPhysicalActivityDataPointMapper.java
index 9ce25e72..eb4378d9 100644
--- a/shim-server/src/main/java/org/openmhealth/shim/runkeeper/mapper/RunkeeperPhysicalActivityDataPointMapper.java
+++ b/shim-server/src/main/java/org/openmhealth/shim/runkeeper/mapper/RunkeeperPhysicalActivityDataPointMapper.java
@@ -54,7 +54,7 @@ private PhysicalActivity getMeasure(JsonNode itemNode) {
PhysicalActivity.Builder builder = new PhysicalActivity.Builder(activityName);
- setEffectiveTimeFrameIfPresent(itemNode, builder);
+ getOptionalTimeFrame(itemNode).ifPresent(builder::setEffectiveTimeFrame);
asOptionalDouble(itemNode, "total_distance")
.ifPresent(distanceInM -> builder.setDistance(new LengthUnitValue(METER, distanceInM)));
diff --git a/shim-server/src/main/java/org/openmhealth/shim/withings/WithingsShim.java b/shim-server/src/main/java/org/openmhealth/shim/withings/WithingsShim.java
index 2d1e54f6..30a613ea 100644
--- a/shim-server/src/main/java/org/openmhealth/shim/withings/WithingsShim.java
+++ b/shim-server/src/main/java/org/openmhealth/shim/withings/WithingsShim.java
@@ -30,7 +30,6 @@
import org.springframework.stereotype.Component;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
-import org.springframework.web.util.UriComponents;
import org.springframework.web.util.UriComponentsBuilder;
import javax.servlet.http.HttpServletRequest;
@@ -43,7 +42,6 @@
import java.util.Map;
import java.util.Objects;
-import static java.util.Collections.singletonList;
import static org.openmhealth.shim.withings.WithingsShim.WithingsDataType.*;
@@ -56,10 +54,10 @@
public class WithingsShim extends OAuth1Shim {
public static final String SHIM_KEY = "withings";
- private static final String DATA_URL = "http://wbsapi.withings.net";
- private static final String REQUEST_TOKEN_URL = "https://oauth.withings.com/account/request_token";
- private static final String USER_AUTHORIZATION_URL = "https://oauth.withings.com/account/authorize";
- private static final String ACCESS_TOKEN_URL = "https://oauth.withings.com/account/access_token";
+ private static final String DATA_URL = "https://api.health.nokia.com";
+ private static final String REQUEST_TOKEN_URL = "https://developer.health.nokia.com/account/request_token";
+ private static final String USER_AUTHORIZATION_URL = "https://developer.health.nokia.com/account/authorize";
+ private static final String ACCESS_TOKEN_URL = "https://developer.health.nokia.com/account/access_token";
private static final String INTRADAY_ACTIVITY_ENDPOINT = "getintradayactivity";
@Autowired
@@ -105,12 +103,14 @@ protected OAuth1ClientSettings getClientSettings() {
public ShimDataType[] getShimDataTypes() {
return new ShimDataType[] {
- BLOOD_PRESSURE,
+ BLOOD_PRESSURE, // order important for trigger call
BODY_HEIGHT,
+ BODY_TEMPERATURE,
BODY_WEIGHT,
CALORIES_BURNED,
HEART_RATE,
SLEEP_DURATION,
+ SLEEP_EPISODE,
STEP_COUNT
};
}
@@ -132,10 +132,12 @@ public enum WithingsDataType implements ShimDataType {
BLOOD_PRESSURE("measure", "getmeas", true),
BODY_HEIGHT("measure", "getmeas", true),
+ BODY_TEMPERATURE("measure", "getmeas", true),
BODY_WEIGHT("measure", "getmeas", true),
CALORIES_BURNED("v2/measure", "getactivity", false),
HEART_RATE("measure", "getmeas", true),
SLEEP_DURATION("v2/sleep", "getsummary", false),
+ SLEEP_EPISODE("v2/sleep", "getsummary", false),
STEP_COUNT("v2/measure", "getactivity", false);
private String endpoint;
@@ -196,50 +198,14 @@ public ShimDataResponse getData(ShimDataRequest shimDataRequest) throws ShimExce
HttpEntity responseEntity = response.getEntity();
if (shimDataRequest.getNormalize()) {
- WithingsDataPointMapper mapper;
-
- switch (withingsDataType) {
-
- case BLOOD_PRESSURE:
- mapper = new WithingsBloodPressureDataPointMapper();
- break;
- case BODY_HEIGHT:
- mapper = new WithingsBodyHeightDataPointMapper();
- break;
- case BODY_WEIGHT:
- mapper = new WithingsBodyWeightDataPointMapper();
- break;
- case CALORIES_BURNED:
- if (clientSettings.isIntradayDataAvailable()) {
- mapper = new WithingsIntradayCaloriesBurnedDataPointMapper();
- }
- else {
- mapper = new WithingsDailyCaloriesBurnedDataPointMapper();
- }
- break;
- case HEART_RATE:
- mapper = new WithingsHeartRateDataPointMapper();
- break;
- case SLEEP_DURATION:
- mapper = new WithingsSleepDurationDataPointMapper();
- break;
- case STEP_COUNT:
- if (clientSettings.isIntradayDataAvailable()) {
- mapper = new WithingsIntradayStepCountDataPointMapper();
- }
- else {
- mapper = new WithingsDailyStepCountDataPointMapper();
- }
- break;
- default:
- throw new UnsupportedOperationException();
- }
+
+ WithingsDataPointMapper> dataPointMapper = getDataPointMapper(withingsDataType);
InputStream content = responseEntity.getContent();
JsonNode jsonNode = objectMapper.readValue(content, JsonNode.class);
- List dataPoints = mapper.asDataPoints(singletonList(jsonNode));
- return ShimDataResponse.result(WithingsShim.SHIM_KEY,
- dataPoints);
+ List extends DataPoint>> dataPoints = dataPointMapper.asDataPoints(jsonNode);
+
+ return ShimDataResponse.result(WithingsShim.SHIM_KEY, dataPoints);
}
else {
return ShimDataResponse
@@ -254,6 +220,50 @@ public ShimDataResponse getData(ShimDataRequest shimDataRequest) throws ShimExce
}
}
+ private WithingsDataPointMapper getDataPointMapper(WithingsDataType withingsDataType) {
+
+ switch (withingsDataType) {
+
+ case BLOOD_PRESSURE:
+ return new WithingsBloodPressureDataPointMapper();
+
+ case BODY_HEIGHT:
+ return new WithingsBodyHeightDataPointMapper();
+
+ case BODY_TEMPERATURE:
+ return new WithingsBodyTemperatureDataPointMapper();
+
+ case BODY_WEIGHT:
+ return new WithingsBodyWeightDataPointMapper();
+
+ case CALORIES_BURNED:
+ if (clientSettings.isIntradayDataAvailable()) {
+ return new WithingsIntradayCaloriesBurnedDataPointMapper();
+ }
+
+ return new WithingsDailyCaloriesBurnedDataPointMapper();
+
+ case HEART_RATE:
+ return new WithingsHeartRateDataPointMapper();
+
+ case SLEEP_DURATION:
+ return new WithingsSleepDurationDataPointMapper();
+
+ case SLEEP_EPISODE:
+ return new WithingsSleepEpisodeDataPointMapper();
+
+ case STEP_COUNT:
+ if (clientSettings.isIntradayDataAvailable()) {
+ return new WithingsIntradayStepCountDataPointMapper();
+ }
+
+ return new WithingsDailyStepCountDataPointMapper();
+
+ default:
+ throw new UnsupportedOperationException();
+ }
+ }
+
URI createWithingsRequestUri(ShimDataRequest shimDataRequest, String userid,
WithingsDataType withingsDataType) {
@@ -269,8 +279,10 @@ URI createWithingsRequestUri(ShimDataRequest shimDataRequest, String userid,
dateTimeMap.add("enddateymd", shimDataRequest.getEndDateTime().toLocalDate().toString());
}
- UriComponentsBuilder uriComponentsBuilder = UriComponentsBuilder.fromUriString(DATA_URL).pathSegment(
- withingsDataType.getEndpoint());
+ UriComponentsBuilder uriComponentsBuilder = UriComponentsBuilder
+ .fromUriString(DATA_URL)
+ .pathSegment(withingsDataType.getEndpoint());
+
String measureParameter;
if (isIntradayActivityMeasure(withingsDataType)) {
// intraday data uses a different endpoint
@@ -279,7 +291,9 @@ URI createWithingsRequestUri(ShimDataRequest shimDataRequest, String userid,
else {
measureParameter = withingsDataType.getMeasureParameter();
}
- uriComponentsBuilder.queryParam("action", measureParameter).queryParam("userid", userid)
+ uriComponentsBuilder
+ .queryParam("action", measureParameter)
+ .queryParam("userid", userid)
.queryParams(dateTimeMap);
// if it's a body measure
@@ -297,17 +311,16 @@ URI createWithingsRequestUri(ShimDataRequest shimDataRequest, String userid,
uriComponentsBuilder.queryParam("meastype", measureType.getMagicNumber());
}
- uriComponentsBuilder.queryParam("category", 1); //filter out goal datapoints
+ uriComponentsBuilder.queryParam("category", 1); // filter out goal data points
}
- UriComponents uriComponents = uriComponentsBuilder.build();
- return uriComponents.toUri();
+ return uriComponentsBuilder.build().toUri();
}
/**
- * Determines whether the request is a Withings intraday request based on the configuration
- * setup and the data type from the Shim API request. This case requires a different endpoint and different time
- * parameters than the standard activity endpoint.
+ * Determines whether the request is a Withings intraday request based on the configuration setup and the data type
+ * from the Shim API request. This case requires a different endpoint and different time parameters than the
+ * standard activity endpoint.
*
* @param withingsDataType the withings data type retrieved from the Shim API request
*/
diff --git a/shim-server/src/main/java/org/openmhealth/shim/withings/domain/WithingsBodyMeasureType.java b/shim-server/src/main/java/org/openmhealth/shim/withings/domain/WithingsBodyMeasureType.java
index 4379651b..2c945a8b 100644
--- a/shim-server/src/main/java/org/openmhealth/shim/withings/domain/WithingsBodyMeasureType.java
+++ b/shim-server/src/main/java/org/openmhealth/shim/withings/domain/WithingsBodyMeasureType.java
@@ -30,13 +30,11 @@ public enum WithingsBodyMeasureType {
BODY_WEIGHT(1),
BODY_HEIGHT(4),
- // FAT_FREE_MASS(5), // TODO confirm what this means
- // FAT_RATIO(6), // TODO confirm what this means
- // FAT_MASS_WEIGHT(8), // TODO confirm what this means
DIASTOLIC_BLOOD_PRESSURE(9),
SYSTOLIC_BLOOD_PRESSURE(10),
HEART_RATE(11),
- OXYGEN_SATURATION(54);
+ OXYGEN_SATURATION(54),
+ BODY_TEMPERATURE(71);
private int magicNumber;
diff --git a/shim-server/src/main/java/org/openmhealth/shim/withings/mapper/WithingsBodyHeightDataPointMapper.java b/shim-server/src/main/java/org/openmhealth/shim/withings/mapper/WithingsBodyHeightDataPointMapper.java
index e2b62ed8..0db4d6cd 100644
--- a/shim-server/src/main/java/org/openmhealth/shim/withings/mapper/WithingsBodyHeightDataPointMapper.java
+++ b/shim-server/src/main/java/org/openmhealth/shim/withings/mapper/WithingsBodyHeightDataPointMapper.java
@@ -24,7 +24,6 @@
import java.math.BigDecimal;
import java.util.Optional;
-import static java.util.Optional.empty;
import static org.openmhealth.schema.domain.omh.LengthUnit.METER;
import static org.openmhealth.shim.withings.domain.WithingsBodyMeasureType.BODY_HEIGHT;
@@ -41,10 +40,6 @@ public class WithingsBodyHeightDataPointMapper extends WithingsBodyMeasureDataPo
Optional value = getValueForMeasureType(measuresNode, BODY_HEIGHT);
- if (!value.isPresent()) {
- return empty();
- }
-
- return Optional.of(new BodyHeight.Builder(new LengthUnitValue(METER, value.get())));
+ return value.map(heightInM -> new BodyHeight.Builder(new LengthUnitValue(METER, heightInM)));
}
}
diff --git a/shim-server/src/main/java/org/openmhealth/shim/withings/mapper/WithingsBodyMeasureDataPointMapper.java b/shim-server/src/main/java/org/openmhealth/shim/withings/mapper/WithingsBodyMeasureDataPointMapper.java
index 854207a3..63641cdb 100644
--- a/shim-server/src/main/java/org/openmhealth/shim/withings/mapper/WithingsBodyMeasureDataPointMapper.java
+++ b/shim-server/src/main/java/org/openmhealth/shim/withings/mapper/WithingsBodyMeasureDataPointMapper.java
@@ -33,6 +33,7 @@
import java.util.stream.Collectors;
import java.util.stream.StreamSupport;
+import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.base.Preconditions.checkNotNull;
import static java.lang.String.format;
import static java.time.ZoneOffset.UTC;
@@ -57,7 +58,7 @@ public abstract class WithingsBodyMeasureDataPointMapper exte
public List> asDataPoints(List responseNodes) {
checkNotNull(responseNodes);
- checkNotNull(responseNodes.size() == 1, "A single response node is allowed per call.");
+ checkArgument(responseNodes.size() == 1, "A single response node is allowed per call.");
JsonNode responseNodeBody = asRequiredNode(responseNodes.get(0), BODY_NODE_PROPERTY);
List> dataPoints = Lists.newArrayList();
@@ -88,14 +89,11 @@ public List> asDataPoints(List responseNodes) {
measureBuilder.setEffectiveTimeFrame(OffsetDateTime.ofInstant(dateTimeInstant, UTC));
}
- Optional userComment = asOptionalString(measureGroupNode, "comment");
- if (userComment.isPresent()) {
- measureBuilder.setUserNotes(userComment.get());
- }
+ asOptionalString(measureGroupNode, "comment").ifPresent(measureBuilder::setUserNotes);
T measure = measureBuilder.build();
- Optional externalId = asOptionalLong(measureGroupNode, "grpid");
+ Optional externalId = asOptionalLong(measureGroupNode, "grpid").map(Object::toString);
dataPoints.add(newDataPoint(measure, externalId.orElse(null), isSensed(measureGroupNode), null));
}
@@ -185,7 +183,6 @@ protected BigDecimal getValue(JsonNode measureNode) {
int scale = asRequiredInteger(measureNode, "unit");
return BigDecimal.valueOf(unscaledValue, -1 * scale);
-
}
/**
diff --git a/shim-server/src/main/java/org/openmhealth/shim/withings/mapper/WithingsBodyTemperatureDataPointMapper.java b/shim-server/src/main/java/org/openmhealth/shim/withings/mapper/WithingsBodyTemperatureDataPointMapper.java
new file mode 100644
index 00000000..1df5f78a
--- /dev/null
+++ b/shim-server/src/main/java/org/openmhealth/shim/withings/mapper/WithingsBodyTemperatureDataPointMapper.java
@@ -0,0 +1,46 @@
+/*
+ * Copyright 2017 Open mHealth
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.openmhealth.shim.withings.mapper;
+
+import com.fasterxml.jackson.databind.JsonNode;
+import org.openmhealth.schema.domain.omh.BodyTemperature;
+import org.openmhealth.schema.domain.omh.Measure;
+import org.openmhealth.schema.domain.omh.TemperatureUnitValue;
+
+import java.math.BigDecimal;
+import java.util.Optional;
+
+import static org.openmhealth.schema.domain.omh.TemperatureUnit.CELSIUS;
+import static org.openmhealth.shim.withings.domain.WithingsBodyMeasureType.BODY_TEMPERATURE;
+
+
+/**
+ * @author Emerson Farrugia
+ * @see Body Measures API documentation
+ */
+public class WithingsBodyTemperatureDataPointMapper extends WithingsBodyMeasureDataPointMapper {
+
+ // TODO since the documentation doesn't explicitly say the temperature is in C, this needs testing
+ @Override
+ public Optional> newMeasureBuilder(JsonNode measuresNode) {
+
+ Optional value = getValueForMeasureType(measuresNode, BODY_TEMPERATURE);
+
+ return value
+ .map(temperatureInC -> new BodyTemperature.Builder(new TemperatureUnitValue(CELSIUS, temperatureInC)));
+ }
+}
diff --git a/shim-server/src/main/java/org/openmhealth/shim/withings/mapper/WithingsBodyWeightDataPointMapper.java b/shim-server/src/main/java/org/openmhealth/shim/withings/mapper/WithingsBodyWeightDataPointMapper.java
index 1ac6123b..33c80ca7 100644
--- a/shim-server/src/main/java/org/openmhealth/shim/withings/mapper/WithingsBodyWeightDataPointMapper.java
+++ b/shim-server/src/main/java/org/openmhealth/shim/withings/mapper/WithingsBodyWeightDataPointMapper.java
@@ -24,7 +24,6 @@
import java.math.BigDecimal;
import java.util.Optional;
-import static java.util.Optional.empty;
import static org.openmhealth.schema.domain.omh.MassUnit.KILOGRAM;
import static org.openmhealth.shim.withings.domain.WithingsBodyMeasureType.BODY_WEIGHT;
@@ -41,10 +40,6 @@ public class WithingsBodyWeightDataPointMapper extends WithingsBodyMeasureDataPo
Optional value = getValueForMeasureType(measuresNode, BODY_WEIGHT);
- if (!value.isPresent()) {
- return empty();
- }
-
- return Optional.of(new BodyWeight.Builder(new MassUnitValue(KILOGRAM, value.get())));
+ return value.map(weightInKg -> new BodyWeight.Builder(new MassUnitValue(KILOGRAM, weightInKg)));
}
}
diff --git a/shim-server/src/main/java/org/openmhealth/shim/withings/mapper/WithingsDailyCaloriesBurnedDataPointMapper.java b/shim-server/src/main/java/org/openmhealth/shim/withings/mapper/WithingsDailyCaloriesBurnedDataPointMapper.java
index 313f6a5e..942a570b 100644
--- a/shim-server/src/main/java/org/openmhealth/shim/withings/mapper/WithingsDailyCaloriesBurnedDataPointMapper.java
+++ b/shim-server/src/main/java/org/openmhealth/shim/withings/mapper/WithingsDailyCaloriesBurnedDataPointMapper.java
@@ -17,85 +17,48 @@
package org.openmhealth.shim.withings.mapper;
import com.fasterxml.jackson.databind.JsonNode;
-import org.openmhealth.schema.domain.omh.*;
+import org.openmhealth.schema.domain.omh.CaloriesBurned2;
+import org.openmhealth.schema.domain.omh.DataPoint;
-import java.time.*;
import java.util.Optional;
-import static org.openmhealth.shim.common.mapper.JsonNodeMappingSupport.asOptionalString;
-import static org.openmhealth.shim.common.mapper.JsonNodeMappingSupport.asRequiredLong;
+import static org.openmhealth.schema.domain.omh.KcalUnit.KILOCALORIE;
+import static org.openmhealth.shim.common.mapper.JsonNodeMappingSupport.*;
/**
- * A mapper from Withings Activity Measures endpoint responses (/measure?action=getactivity) to {@link CaloriesBurned}
- * objects
- *
- *
NOTE: This only captures calories that are burned from activity that is captured by a Withings device or
- * application, and
- * may not be an accurate representation of all the calories burned from metabolic resting or activities not
- * captured.
+ * A mapper from Withings Activity Measures endpoint responses (/measure?action=getactivity) to {@link CaloriesBurned2}
+ * objects.
*
* @author Chris Schaefbauer
+ * @author Emerson Farrugia
* @see Activity Measures API documentation
*/
-public class WithingsDailyCaloriesBurnedDataPointMapper extends WithingsListDataPointMapper {
+public class WithingsDailyCaloriesBurnedDataPointMapper extends WithingsListDataPointMapper {
+
+ @Override
+ String getListNodeName() {
+ return "activities";
+ }
/**
* Maps an individual list node from the array in the Withings activity measure endpoint response into a {@link
- * CaloriesBurned} data point.
- *
- *
Note: the start datetime and end datetime values for the mapped {@link CaloriesBurned} {@link DataPoint}
- * assume that
- * the start timezone and end time zone are the same, both equal to the "timezone" property in the Withings
- * response
- * datapoints. However, according to Withings, the property value they provide is specifically the end datetime
- * timezone.
+ * CaloriesBurned2} data point.
*
* @param node activity node from the array "activites" contained in the "body" of the endpoint response
- * @return a {@link DataPoint} object containing a {@link CaloriesBurned} measure with the appropriate values from
- * the JSON node parameter, wrapped as an {@link Optional}
*/
@Override
- Optional> asDataPoint(JsonNode node) {
+ public Optional> asDataPoint(JsonNode node) {
long caloriesBurnedValue = asRequiredLong(node, "calories");
- CaloriesBurned.Builder caloriesBurnedBuilder =
- new CaloriesBurned.Builder(new KcalUnitValue(KcalUnit.KILOCALORIE, caloriesBurnedValue));
-
- Optional dateString = asOptionalString(node, "date");
- Optional timeZoneFullName = asOptionalString(node, "timezone");
- // We assume that timezone is the same for both the startdate and enddate timestamps, even though Withings only
- // provides the enddate timezone as the "timezone" property.
- // TODO: Revisit once Withings can provide start_timezone and end_timezone
- if (dateString.isPresent() && timeZoneFullName.isPresent()) {
- LocalDateTime localStartDateTime = LocalDate.parse(dateString.get()).atStartOfDay();
- ZoneId zoneId = ZoneId.of(timeZoneFullName.get());
- ZonedDateTime zonedDateTime = ZonedDateTime.of(localStartDateTime, zoneId);
- ZoneOffset offset = zonedDateTime.getOffset();
- OffsetDateTime offsetStartDateTime = OffsetDateTime.of(localStartDateTime, offset);
- LocalDateTime localEndDateTime = LocalDate.parse(dateString.get()).atStartOfDay().plusDays(1);
- OffsetDateTime offsetEndDateTime = OffsetDateTime.of(localEndDateTime, offset);
- caloriesBurnedBuilder.setEffectiveTimeFrame(
- TimeInterval.ofStartDateTimeAndEndDateTime(offsetStartDateTime,
- offsetEndDateTime));
- }
+ CaloriesBurned2.Builder caloriesBurnedBuilder =
+ new CaloriesBurned2.Builder(KILOCALORIE.newUnitValue(caloriesBurnedValue), getTimeFrame(node));
- Optional userComment = asOptionalString(node, "comment");
- if (userComment.isPresent()) {
- caloriesBurnedBuilder.setUserNotes(userComment.get());
- }
+ asOptionalString(node, "comment").ifPresent(caloriesBurnedBuilder::setUserNotes);
- CaloriesBurned caloriesBurned = caloriesBurnedBuilder.build();
- DataPoint caloriesBurnedDataPoint =
- newDataPoint(caloriesBurned, null, true, null);
+ CaloriesBurned2 caloriesBurned = caloriesBurnedBuilder.build();
- return Optional.of(caloriesBurnedDataPoint);
-
- }
-
- @Override
- String getListNodeName() {
- return "activities";
+ return Optional.of(newDataPoint(caloriesBurned, asRequiredString(node, "date"), true, null));
}
}
diff --git a/shim-server/src/main/java/org/openmhealth/shim/withings/mapper/WithingsDailyStepCountDataPointMapper.java b/shim-server/src/main/java/org/openmhealth/shim/withings/mapper/WithingsDailyStepCountDataPointMapper.java
index 2f94ef66..d2385c51 100644
--- a/shim-server/src/main/java/org/openmhealth/shim/withings/mapper/WithingsDailyStepCountDataPointMapper.java
+++ b/shim-server/src/main/java/org/openmhealth/shim/withings/mapper/WithingsDailyStepCountDataPointMapper.java
@@ -18,73 +18,45 @@
import com.fasterxml.jackson.databind.JsonNode;
import org.openmhealth.schema.domain.omh.DataPoint;
-import org.openmhealth.schema.domain.omh.StepCount1;
-import org.openmhealth.schema.domain.omh.TimeInterval;
+import org.openmhealth.schema.domain.omh.StepCount2;
-import java.time.*;
import java.util.Optional;
-import static org.openmhealth.shim.common.mapper.JsonNodeMappingSupport.asOptionalString;
-import static org.openmhealth.shim.common.mapper.JsonNodeMappingSupport.asRequiredLong;
+import static org.openmhealth.shim.common.mapper.JsonNodeMappingSupport.*;
/**
- * A mapper from Withings Activity Measures endpoint responses (/measure?action=getactivity) to {@link StepCount1}
+ * A mapper from Withings Activity Measures endpoint responses (/measure?action=getactivity) to {@link StepCount2}
* objects.
- *
- *
Note: the start datetime and end datetime values for the mapped {@link StepCount1} {@link DataPoint} assume that
- * the start timezone and end time zone are the same, both equal to the "timezone" property in the Withings response
- * datapoints. However, according to Withings, the property value they provide is specifically the end datetime
- * timezone.
*
* @author Chris Schaefbauer
+ * @author Emerson Farrugia
* @see Activity Measures API documentation
*/
-public class WithingsDailyStepCountDataPointMapper extends WithingsListDataPointMapper {
+public class WithingsDailyStepCountDataPointMapper extends WithingsListDataPointMapper {
+
+ @Override
+ String getListNodeName() {
+ return "activities";
+ }
/**
* Maps an individual list node from the array in the Withings activity measure endpoint response into a {@link
- * StepCount1} data point.
+ * StepCount2} data point.
*
* @param node activity node from the array "activites" contained in the "body" of the endpoint response
- * @return a {@link DataPoint} object containing a {@link StepCount1} measure with the appropriate values from
- * the JSON node parameter, wrapped as an {@link Optional}
*/
@Override
- Optional> asDataPoint(JsonNode node) {
+ Optional> asDataPoint(JsonNode node) {
long stepValue = asRequiredLong(node, "steps");
- StepCount1.Builder stepCountBuilder = new StepCount1.Builder(stepValue);
- Optional dateString = asOptionalString(node, "date");
- Optional timeZoneFullName = asOptionalString(node, "timezone");
- // We assume that timezone is the same for both the startdate and enddate timestamps, even though Withings only
- // provides the enddate timezone as the "timezone" property.
- // TODO: Revisit once Withings can provide start_timezone and end_timezone
- if (dateString.isPresent() && timeZoneFullName.isPresent()) {
- LocalDateTime localStartDateTime = LocalDate.parse(dateString.get()).atStartOfDay();
- ZoneId zoneId = ZoneId.of(timeZoneFullName.get());
- ZonedDateTime zonedDateTime = ZonedDateTime.of(localStartDateTime, zoneId);
- ZoneOffset offset = zonedDateTime.getOffset();
- OffsetDateTime offsetStartDateTime = OffsetDateTime.of(localStartDateTime, offset);
- LocalDateTime localEndDateTime = LocalDate.parse(dateString.get()).atStartOfDay().plusDays(1);
- OffsetDateTime offsetEndDateTime = OffsetDateTime.of(localEndDateTime, offset);
- stepCountBuilder.setEffectiveTimeFrame(
- TimeInterval.ofStartDateTimeAndEndDateTime(offsetStartDateTime, offsetEndDateTime));
- }
- Optional userComment = asOptionalString(node, "comment");
- if (userComment.isPresent()) {
- stepCountBuilder.setUserNotes(userComment.get());
- }
+ StepCount2.Builder stepCountBuilder = new StepCount2.Builder(stepValue, getTimeFrame(node));
- StepCount1 stepCount = stepCountBuilder.build();
- DataPoint stepCountDataPoint = newDataPoint(stepCount, null, true, null);
+ asOptionalString(node, "comment").ifPresent(stepCountBuilder::setUserNotes);
- return Optional.of(stepCountDataPoint);
- }
+ StepCount2 stepCount = stepCountBuilder.build();
- @Override
- String getListNodeName() {
- return "activities";
+ return Optional.of(newDataPoint(stepCount, asRequiredString(node, "date"), true, null));
}
}
diff --git a/shim-server/src/main/java/org/openmhealth/shim/withings/mapper/WithingsDataPointMapper.java b/shim-server/src/main/java/org/openmhealth/shim/withings/mapper/WithingsDataPointMapper.java
index 8bcf1851..4bb0bc17 100644
--- a/shim-server/src/main/java/org/openmhealth/shim/withings/mapper/WithingsDataPointMapper.java
+++ b/shim-server/src/main/java/org/openmhealth/shim/withings/mapper/WithingsDataPointMapper.java
@@ -16,9 +16,16 @@
package org.openmhealth.shim.withings.mapper;
-import org.openmhealth.schema.domain.omh.*;
+import org.openmhealth.schema.domain.omh.DataPoint;
+import org.openmhealth.schema.domain.omh.DataPointAcquisitionProvenance;
+import org.openmhealth.schema.domain.omh.DataPointHeader;
+import org.openmhealth.schema.domain.omh.Measure;
import org.openmhealth.shim.common.mapper.JsonNodeDataPointMapper;
+import java.time.Instant;
+import java.time.OffsetDateTime;
+import java.time.ZoneId;
+
import static java.util.UUID.randomUUID;
import static org.openmhealth.schema.domain.omh.DataPointModality.SELF_REPORTED;
import static org.openmhealth.schema.domain.omh.DataPointModality.SENSED;
@@ -27,7 +34,7 @@
/**
* @author Chris Schaefbauer
*/
-public abstract class WithingsDataPointMapper implements JsonNodeDataPointMapper {
+public abstract class WithingsDataPointMapper implements JsonNodeDataPointMapper {
public final static String RESOURCE_API_SOURCE_NAME = "Withings Resource API";
protected static final String BODY_NODE_PROPERTY = "body";
@@ -38,11 +45,11 @@ public abstract class WithingsDataPointMapper implement
* @param measure a measure
* @param externalId the Withings identifier of the measure, if known
* @param sensed a boolean indicating whether the measure was sensed by a device, if known
- * @param deviceName the name of the Withings device that generated the measure, if known
+ * @param device the Withings device that generated the measure, if known
* @return the constructed data point
*/
- protected DataPoint newDataPoint(T measure, Long externalId, Boolean sensed,
- String deviceName) {
+ protected DataPoint newDataPoint(T measure, String externalId, Boolean sensed,
+ WithingsDevice device) {
DataPointAcquisitionProvenance.Builder provenanceBuilder =
new DataPointAcquisitionProvenance.Builder(RESOURCE_API_SOURCE_NAME);
@@ -54,8 +61,8 @@ protected DataPoint newDataPoint(T measure, Long external
// additional properties are always subject to change
DataPointAcquisitionProvenance acquisitionProvenance = provenanceBuilder.build();
- if (deviceName != null) {
- acquisitionProvenance.setAdditionalProperty("device_name", deviceName);
+ if (device != null) {
+ acquisitionProvenance.setAdditionalProperty("device_name", device.getDisplayName());
}
if (externalId != null) {
@@ -69,4 +76,9 @@ protected DataPoint newDataPoint(T measure, Long external
return new DataPoint<>(header, measure);
}
+
+ protected OffsetDateTime asOffsetDateTime(long epochSeconds, String timeZoneId) {
+
+ return Instant.ofEpochSecond(epochSeconds).atZone(ZoneId.of(timeZoneId)).toOffsetDateTime();
+ }
}
diff --git a/shim-server/src/main/java/org/openmhealth/shim/withings/mapper/WithingsDevice.java b/shim-server/src/main/java/org/openmhealth/shim/withings/mapper/WithingsDevice.java
new file mode 100644
index 00000000..bcb40f33
--- /dev/null
+++ b/shim-server/src/main/java/org/openmhealth/shim/withings/mapper/WithingsDevice.java
@@ -0,0 +1,61 @@
+/*
+ * Copyright 2017 Open mHealth
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.openmhealth.shim.withings.mapper;
+
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Optional;
+
+
+/**
+ * @author Chris Schaefbauer
+ * @author Emerson Farrugia
+ */
+public enum WithingsDevice {
+
+ ACTIVITY_TRACKER(16, "Activity tracker"),
+ AURA(32, "Aura");
+
+ private int magicNumber;
+ private String displayName;
+
+ private static Map map = new HashMap<>();
+
+ static {
+ for (WithingsDevice constant : WithingsDevice.values()) {
+ map.put(constant.magicNumber, constant);
+ }
+ }
+
+ WithingsDevice(int magicNumber, String displayName) {
+ this.magicNumber = magicNumber;
+ this.displayName = displayName;
+ }
+
+ public String getDisplayName() {
+ return displayName;
+ }
+
+ /**
+ * @param magicNumber a magic number
+ * @return the constant corresponding to the magic number
+ */
+ public static Optional findByMagicNumber(Integer magicNumber) {
+
+ return Optional.ofNullable(map.get(magicNumber));
+ }
+}
diff --git a/shim-server/src/main/java/org/openmhealth/shim/withings/mapper/WithingsHeartRateDataPointMapper.java b/shim-server/src/main/java/org/openmhealth/shim/withings/mapper/WithingsHeartRateDataPointMapper.java
index d08ede5f..c122d195 100644
--- a/shim-server/src/main/java/org/openmhealth/shim/withings/mapper/WithingsHeartRateDataPointMapper.java
+++ b/shim-server/src/main/java/org/openmhealth/shim/withings/mapper/WithingsHeartRateDataPointMapper.java
@@ -23,7 +23,6 @@
import java.math.BigDecimal;
import java.util.Optional;
-import static java.util.Optional.empty;
import static org.openmhealth.shim.withings.domain.WithingsBodyMeasureType.HEART_RATE;
@@ -39,10 +38,6 @@ public class WithingsHeartRateDataPointMapper extends WithingsBodyMeasureDataPoi
Optional value = getValueForMeasureType(measuresNode, HEART_RATE);
- if (!value.isPresent()) {
- return empty();
- }
-
- return Optional.of(new HeartRate.Builder(value.get()));
+ return value.map(HeartRate.Builder::new);
}
}
diff --git a/shim-server/src/main/java/org/openmhealth/shim/withings/mapper/WithingsIntradayCaloriesBurnedDataPointMapper.java b/shim-server/src/main/java/org/openmhealth/shim/withings/mapper/WithingsIntradayCaloriesBurnedDataPointMapper.java
index 03d53ae5..672561d4 100644
--- a/shim-server/src/main/java/org/openmhealth/shim/withings/mapper/WithingsIntradayCaloriesBurnedDataPointMapper.java
+++ b/shim-server/src/main/java/org/openmhealth/shim/withings/mapper/WithingsIntradayCaloriesBurnedDataPointMapper.java
@@ -16,141 +16,34 @@
package org.openmhealth.shim.withings.mapper;
-import com.fasterxml.jackson.databind.JsonNode;
-import com.google.common.collect.Lists;
-import com.google.common.collect.Maps;
-import org.openmhealth.schema.domain.omh.*;
+import org.openmhealth.schema.domain.omh.CaloriesBurned2;
+import org.openmhealth.schema.domain.omh.TimeFrame;
-import java.time.Instant;
-import java.time.OffsetDateTime;
-import java.time.ZoneId;
-import java.util.*;
-
-import static com.google.common.base.Preconditions.checkNotNull;
-import static org.openmhealth.shim.common.mapper.JsonNodeMappingSupport.*;
+import static org.openmhealth.schema.domain.omh.KcalUnit.KILOCALORIE;
/**
- * A mapper from Withings Intraday Activity endpoint responses (/measure?action=getactivity) to {@link CaloriesBurned}
+ * A mapper from Withings Intraday Activity endpoint responses (/measure?action=getactivity) to {@link CaloriesBurned2}
* objects.
*
*
This mapper handles responses from an API request that requires special permissions from Withings. This special
- * activation can be requested from their API
- * Documentation website
*
+ * @author Emerson Farrugia
* @author Chris Schaefbauer
- * @see Intrday Activity Measures API
- * documentation
+ * @see Activity Measures API documentation
*/
-public class WithingsIntradayCaloriesBurnedDataPointMapper extends WithingsDataPointMapper {
+public class WithingsIntradayCaloriesBurnedDataPointMapper extends WithingsIntradayDataPointMapper {
- /**
- * Maps JSON response nodes from the intraday activities endpoint (measure?action=getintradayactivity) in the
- * Withings API into a list of {@link CaloriesBurned} {@link DataPoint} objects.
- *
- * @param responseNodes a list of a single JSON node containing the entire response from the intraday activities
- * endpoint
- * @return a list of DataPoint objects of type {@link CaloriesBurned} with the appropriate values mapped from the
- * input
- * JSON
- */
@Override
- public List> asDataPoints(List responseNodes) {
-
- checkNotNull(responseNodes);
- checkNotNull(responseNodes.size() == 1, "A single response node is allowed per call.");
-
- JsonNode bodyNode = asRequiredNode(responseNodes.get(0), "body");
- JsonNode seriesNode = asRequiredNode(bodyNode, "series");
-
- Iterator> fieldsIterator = seriesNode.fields();
- Map nodesWithCalories = getNodesWithCalories(fieldsIterator);
- List startDateTimesInUnixEpochSeconds = Lists.newArrayList(nodesWithCalories.keySet());
-
- //ensure the datapoints are in order of passing time (data points that are earlier in time come before data
- // points that are later)
- Collections.sort(startDateTimesInUnixEpochSeconds);
- ArrayList> dataPoints = Lists.newArrayList();
- for (Long startDateTime : startDateTimesInUnixEpochSeconds) {
- asDataPoint(nodesWithCalories.get(startDateTime), startDateTime).ifPresent(dataPoints::add);
- }
-
- return dataPoints;
-
- }
-
- /**
- * Maps an individual list node from the array in the Withings activity measure endpoint response into a {@link
- * CaloriesBurned} data point.
- *
- * @param nodeWithCalorie activity node from the array "activites" contained in the "body" of the endpoint response
- * that has a calories field
- * @return a {@link DataPoint} object containing a {@link CaloriesBurned} measure with the appropriate values from
- * the JSON node parameter, wrapped as an {@link Optional}
- */
- private Optional> asDataPoint(JsonNode nodeWithCalorie,
- Long startDateTimeInUnixEpochSeconds) {
-
- Long caloriesBurnedValue = asRequiredLong(nodeWithCalorie, "calories");
- CaloriesBurned.Builder caloriesBurnedBuilder =
- new CaloriesBurned.Builder(new KcalUnitValue(KcalUnit.KILOCALORIE, caloriesBurnedValue));
-
- Optional duration = asOptionalLong(nodeWithCalorie, "duration");
- if (duration.isPresent()) {
- OffsetDateTime offsetDateTime =
- OffsetDateTime.ofInstant(Instant.ofEpochSecond(startDateTimeInUnixEpochSeconds), ZoneId.of("Z"));
- caloriesBurnedBuilder.setEffectiveTimeFrame(
- TimeInterval.ofStartDateTimeAndDuration(offsetDateTime, new DurationUnitValue(
- DurationUnit.SECOND, duration.get())));
- }
-
- Optional userComment = asOptionalString(nodeWithCalorie, "comment");
- if (userComment.isPresent()) {
- caloriesBurnedBuilder.setUserNotes(userComment.get());
- }
-
- CaloriesBurned calorieBurned = caloriesBurnedBuilder.build();
- return Optional.of(newDataPoint(calorieBurned, null, true,
- null));
-
+ public String getMeasureValuePath() {
+ return "calories";
}
- /**
- * Creates a hashmap that contains only the entries from the intraday activities dictionary that have calories
- * burned counts.
- *
- * @param fieldsIterator an iterator of map entries containing the key-value pairs related to each intraday
- * activity event
- * @return a hashmap with keys as the start datetime (in unix epoch seconds) of each activity event, and values as
- * the information related to the activity event starting at the key datetime
- */
- private Map getNodesWithCalories(Iterator> fieldsIterator) {
-
- HashMap nodesWithCalories = Maps.newHashMap();
- fieldsIterator.forEachRemaining(n -> addNodesIfHasCalories(nodesWithCalories, n));
- return nodesWithCalories;
-
- }
-
- /**
- * Adds a key-value entry into the nodesWithCalories hashmap if it has a calories value.
- *
- * @param nodesWithCalories pass by reference hashmap to which the key-value pair should be added if a calories
- * value exists
- * @param intradayActivityEventEntry an entry from the intraday activity series dictionary, the key is a string
- * representing the state datetime for the acivity period (in unix epoch seconds) and the value is the JSON object
- * holding data related to that activity
- */
- private void addNodesIfHasCalories(HashMap nodesWithCalories,
- Map.Entry intradayActivityEventEntry) {
-
- if (intradayActivityEventEntry.getValue().has("calories")) {
- nodesWithCalories
- .put(Long.parseLong(intradayActivityEventEntry.getKey()), intradayActivityEventEntry.getValue());
- }
+ @Override
+ public CaloriesBurned2 newMeasure(Long measureValue, TimeFrame effectiveTimeFrame) {
+ return new CaloriesBurned2.Builder(KILOCALORIE.newUnitValue(measureValue), effectiveTimeFrame).build();
}
-
-
}
diff --git a/shim-server/src/main/java/org/openmhealth/shim/withings/mapper/WithingsIntradayDataPointMapper.java b/shim-server/src/main/java/org/openmhealth/shim/withings/mapper/WithingsIntradayDataPointMapper.java
new file mode 100644
index 00000000..5e8afd86
--- /dev/null
+++ b/shim-server/src/main/java/org/openmhealth/shim/withings/mapper/WithingsIntradayDataPointMapper.java
@@ -0,0 +1,117 @@
+/*
+ * Copyright 2017 Open mHealth
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.openmhealth.shim.withings.mapper;
+
+import com.fasterxml.jackson.databind.JsonNode;
+import com.google.common.collect.Streams;
+import org.openmhealth.schema.domain.omh.DataPoint;
+import org.openmhealth.schema.domain.omh.Measure;
+import org.openmhealth.schema.domain.omh.TimeFrame;
+import org.openmhealth.schema.domain.omh.TimeInterval;
+
+import java.time.Instant;
+import java.time.OffsetDateTime;
+import java.util.List;
+import java.util.stream.Collectors;
+
+import static com.google.common.base.Preconditions.checkArgument;
+import static com.google.common.base.Preconditions.checkNotNull;
+import static java.time.ZoneOffset.UTC;
+import static org.openmhealth.schema.domain.omh.DurationUnit.SECOND;
+import static org.openmhealth.shim.common.mapper.JsonNodeMappingSupport.asRequiredLong;
+import static org.openmhealth.shim.common.mapper.JsonNodeMappingSupport.asRequiredNode;
+
+
+/**
+ * A mapper from Withings Intraday Activity endpoint responses (/measure?action=getactivity) to measure
+ * objects.
+ *
+ * This mapper handles responses from an API request that requires special permissions from Withings. This special
+ * activation can be requested from their API
+ * Documentation website
+ *
+ * @author Emerson Farrugia
+ * @author Chris Schaefbauer
+ * @see Intrday Activity Measures API
+ * documentation
+ */
+public abstract class WithingsIntradayDataPointMapper extends WithingsDataPointMapper {
+
+ /**
+ * Maps JSON response nodes from the intraday activities endpoint (measure?action=getintradayactivity) in the
+ * Withings API into a list of data points.
+ *
+ * @param responseNodes a list of a single JSON node containing the entire response from the intraday activities
+ * endpoint
+ */
+ @Override
+ public List> asDataPoints(List responseNodes) {
+
+ checkNotNull(responseNodes);
+ checkArgument(responseNodes.size() == 1, "A single response node is allowed per call.");
+
+ JsonNode bodyNode = asRequiredNode(responseNodes.get(0), "body");
+ JsonNode seriesNode = asRequiredNode(bodyNode, "series");
+
+ return Streams.stream(seriesNode.fields())
+ .filter((e) -> e.getValue().hasNonNull(getMeasureValuePath()))
+ .map(e -> {
+ MeasureTuple tuple = new MeasureTuple();
+
+ tuple.startDateTime = Instant.ofEpochSecond(Long.valueOf(e.getKey())).atOffset(UTC);
+ tuple.durationInSeconds = asRequiredLong(e.getValue(), "duration");
+ tuple.measureValue = asRequiredLong(e.getValue(), getMeasureValuePath());
+
+ return tuple;
+ })
+ .filter(p -> p.measureValue >= 0)
+ .sorted()
+ .map(p -> newMeasure(p.measureValue, p.getTimeFrame()))
+ .map(m -> {
+ String externalId = String.valueOf(
+ m.getEffectiveTimeFrame().getTimeInterval().getStartDateTime().toEpochSecond());
+
+ return newDataPoint(m, externalId, true, null);
+ })
+ .collect(Collectors.toList());
+ }
+
+ /**
+ * @return the path of the node that contains the measure value
+ */
+ public abstract String getMeasureValuePath();
+
+
+ class MeasureTuple implements Comparable {
+
+ public OffsetDateTime startDateTime;
+ public long durationInSeconds;
+ public long measureValue;
+
+ public TimeFrame getTimeFrame() {
+ return new TimeFrame(
+ TimeInterval.ofStartDateTimeAndDuration(startDateTime, SECOND.newUnitValue(durationInSeconds)));
+ }
+
+ @Override
+ public int compareTo(MeasureTuple o) {
+ return this.startDateTime.compareTo(o.startDateTime);
+ }
+ }
+
+ public abstract T newMeasure(Long measureValue, TimeFrame effectiveTimeFrame);
+}
diff --git a/shim-server/src/main/java/org/openmhealth/shim/withings/mapper/WithingsIntradayStepCountDataPointMapper.java b/shim-server/src/main/java/org/openmhealth/shim/withings/mapper/WithingsIntradayStepCountDataPointMapper.java
index bb6b8c6c..e4251f30 100644
--- a/shim-server/src/main/java/org/openmhealth/shim/withings/mapper/WithingsIntradayStepCountDataPointMapper.java
+++ b/shim-server/src/main/java/org/openmhealth/shim/withings/mapper/WithingsIntradayStepCountDataPointMapper.java
@@ -16,128 +16,32 @@
package org.openmhealth.shim.withings.mapper;
-import com.fasterxml.jackson.databind.JsonNode;
-import com.google.common.collect.Lists;
-import com.google.common.collect.Maps;
-import org.openmhealth.schema.domain.omh.*;
-
-import java.time.Instant;
-import java.time.OffsetDateTime;
-import java.time.ZoneId;
-import java.util.*;
-
-import static com.google.common.base.Preconditions.checkNotNull;
-import static org.openmhealth.shim.common.mapper.JsonNodeMappingSupport.*;
+import org.openmhealth.schema.domain.omh.StepCount2;
+import org.openmhealth.schema.domain.omh.TimeFrame;
/**
- * A mapper from Withings Intraday Activity endpoint responses (/measure?action=getactivity) to {@link StepCount1}
+ * A mapper from Withings Intraday Activity endpoint responses (/measure?action=getactivity) to {@link StepCount2}
* objects.
*
*
This mapper handles responses from an API request that requires special permissions from Withings. This special
* activation can be requested by filling the form linked from their API Documentation website
*
+ * @author Emerson Farrugia
* @author Chris Schaefbauer
* @see Activity Measures API documentation
*/
-public class WithingsIntradayStepCountDataPointMapper extends WithingsDataPointMapper {
+public class WithingsIntradayStepCountDataPointMapper extends WithingsIntradayDataPointMapper {
- /**
- * Maps JSON response nodes from the intraday activities endpoint (measure?action=getintradayactivity) in the
- * Withings API into a list of {@link StepCount1} {@link DataPoint} objects.
- *
- * @param responseNodes a list of a single JSON node containing the entire response from the intraday activities
- * endpoint
- * @return a list of DataPoint objects of type {@link StepCount1} with the appropriate values mapped from the input
- * JSON
- */
@Override
- public List> asDataPoints(List responseNodes) {
-
- checkNotNull(responseNodes);
- checkNotNull(responseNodes.size() == 1, "A single response node is allowed per call.");
-
- JsonNode bodyNode = asRequiredNode(responseNodes.get(0), "body");
- JsonNode seriesNode = asRequiredNode(bodyNode, "series");
-
- Iterator> fieldsIterator = seriesNode.fields();
- Map nodesWithSteps = getNodesWithSteps(fieldsIterator);
- List startDateTimesInUnixEpochSeconds = Lists.newArrayList(nodesWithSteps.keySet());
-
- //ensure the datapoints are in order of passing time (data points that are earlier in time come before data
- // points that are later)
- Collections.sort(startDateTimesInUnixEpochSeconds);
- ArrayList> dataPoints = Lists.newArrayList();
- for (Long startDateTime : startDateTimesInUnixEpochSeconds) {
- asDataPoint(nodesWithSteps.get(startDateTime), startDateTime).ifPresent(dataPoints::add);
- }
-
- return dataPoints;
-
+ public String getMeasureValuePath() {
+ return "steps";
}
- /**
- * Maps an individual list node from the array in the Withings activity measure endpoint response into a {@link
- * StepCount1} data point.
- *
- * @param nodeWithSteps activity node from the array "activites" contained in the "body" of the endpoint response
- * @return a {@link DataPoint} object containing a {@link StepCount1} measure with the appropriate values from
- * the JSON node parameter, wrapped as an {@link Optional}
- */
- private Optional> asDataPoint(JsonNode nodeWithSteps, Long startDateTimeInUnixEpochSeconds) {
- Long stepCountValue = asRequiredLong(nodeWithSteps, "steps");
- StepCount1.Builder stepCountBuilder = new StepCount1.Builder(stepCountValue);
-
- Optional duration = asOptionalLong(nodeWithSteps, "duration");
- if (duration.isPresent()) {
- OffsetDateTime offsetDateTime =
- OffsetDateTime.ofInstant(Instant.ofEpochSecond(startDateTimeInUnixEpochSeconds), ZoneId.of("Z"));
- stepCountBuilder.setEffectiveTimeFrame(
- TimeInterval.ofStartDateTimeAndDuration(offsetDateTime, new DurationUnitValue(
- DurationUnit.SECOND, duration.get())));
- }
-
- Optional userComment = asOptionalString(nodeWithSteps, "comment");
- if (userComment.isPresent()) {
- stepCountBuilder.setUserNotes(userComment.get());
- }
-
- StepCount1 stepCount = stepCountBuilder.build();
- return Optional.of(newDataPoint(stepCount, null, true, null));
- }
-
- /**
- * Creates a map that contains only the entries from the intraday activities dictionary that have step counts.
- *
- * @param fieldsIterator an iterator of map entries containing the key-value pairs related to each intraday
- * activity
- * event
- * @return a map with keys as the start datetime (in unix epoch seconds) of each activity event, and values as
- * the information related to the activity event starting at the key datetime
- */
- private Map getNodesWithSteps(Iterator> fieldsIterator) {
- HashMap nodesWithSteps = Maps.newHashMap();
- fieldsIterator.forEachRemaining(n -> addNodeIfHasSteps(nodesWithSteps, n));
- return nodesWithSteps;
- }
+ @Override
+ public StepCount2 newMeasure(Long measureValue, TimeFrame effectiveTimeFrame) {
- /**
- * Adds a key-value entry into the nodesWithStepValue hashmap if it has a steps value.
- *
- * @param nodesWithStepValue pass by reference hashmap to which the key-value pair should be added if a step count
- * value exists
- * @param intradayActivityEventEntry an entry from the intraday activity series dictionary, the key is a string
- * representing the state datetime for the acivity period (in unix epoch seconds) and the value is the JSON object
- * holding data related to that activity
- */
- private void addNodeIfHasSteps(HashMap nodesWithStepValue,
- Map.Entry intradayActivityEventEntry) {
- if (intradayActivityEventEntry.getValue().has("steps")) {
- nodesWithStepValue
- .put(Long.parseLong(intradayActivityEventEntry.getKey()), intradayActivityEventEntry.getValue());
- }
+ return new StepCount2.Builder(measureValue, effectiveTimeFrame).build();
}
-
-
}
diff --git a/shim-server/src/main/java/org/openmhealth/shim/withings/mapper/WithingsListDataPointMapper.java b/shim-server/src/main/java/org/openmhealth/shim/withings/mapper/WithingsListDataPointMapper.java
index dcb0c65f..c88805b6 100644
--- a/shim-server/src/main/java/org/openmhealth/shim/withings/mapper/WithingsListDataPointMapper.java
+++ b/shim-server/src/main/java/org/openmhealth/shim/withings/mapper/WithingsListDataPointMapper.java
@@ -21,12 +21,20 @@
import org.openmhealth.schema.domain.omh.DataPoint;
import org.openmhealth.schema.domain.omh.Measure;
import org.openmhealth.schema.domain.omh.SchemaSupport;
+import org.openmhealth.schema.domain.omh.TimeFrame;
+import java.time.LocalDate;
+import java.time.OffsetDateTime;
+import java.time.ZoneId;
+import java.time.ZonedDateTime;
import java.util.List;
import java.util.Optional;
import static com.google.common.base.Preconditions.checkNotNull;
+import static java.time.LocalTime.MIDNIGHT;
+import static org.openmhealth.schema.domain.omh.TimeInterval.ofStartDateTimeAndEndDateTime;
import static org.openmhealth.shim.common.mapper.JsonNodeMappingSupport.asRequiredNode;
+import static org.openmhealth.shim.common.mapper.JsonNodeMappingSupport.asRequiredString;
/**
@@ -36,7 +44,7 @@
* @author Chris Schaefbauer
* @author Emerson Farrugia
*/
-public abstract class WithingsListDataPointMapper extends WithingsDataPointMapper {
+public abstract class WithingsListDataPointMapper extends WithingsDataPointMapper {
/**
* Maps a JSON response with individual data points contained in a JSON array to a list of {@link DataPoint}
@@ -81,4 +89,17 @@ public List> asDataPoints(List responseNodes) {
* @return the name of the list node used by this mapper
*/
abstract String getListNodeName();
+
+ protected TimeFrame getTimeFrame(JsonNode node) {
+
+ LocalDate localDate = LocalDate.parse(asRequiredString(node, "date"));
+
+ // We assume that timezone is the same for both the startdate and enddate timestamps, even though Withings only
+ // provides the enddate timezone as the "timezone" property.
+ ZoneId timeZoneId = ZoneId.of(asRequiredString(node, "timezone"));
+
+ OffsetDateTime startDateTime = ZonedDateTime.of(localDate, MIDNIGHT, timeZoneId).toOffsetDateTime();
+
+ return new TimeFrame(ofStartDateTimeAndEndDateTime(startDateTime, startDateTime.plusDays(1)));
+ }
}
diff --git a/shim-server/src/main/java/org/openmhealth/shim/withings/mapper/WithingsSleepDurationDataPointMapper.java b/shim-server/src/main/java/org/openmhealth/shim/withings/mapper/WithingsSleepDurationDataPointMapper.java
index cdf9adb4..d98aa3b3 100644
--- a/shim-server/src/main/java/org/openmhealth/shim/withings/mapper/WithingsSleepDurationDataPointMapper.java
+++ b/shim-server/src/main/java/org/openmhealth/shim/withings/mapper/WithingsSleepDurationDataPointMapper.java
@@ -17,128 +17,63 @@
package org.openmhealth.shim.withings.mapper;
import com.fasterxml.jackson.databind.JsonNode;
-import org.openmhealth.schema.domain.omh.*;
+import org.openmhealth.schema.domain.omh.DataPoint;
+import org.openmhealth.schema.domain.omh.SleepDuration2;
+import org.openmhealth.schema.domain.omh.SleepEpisode;
-import java.time.Instant;
-import java.time.OffsetDateTime;
-import java.util.HashMap;
-import java.util.Map;
import java.util.Optional;
-import static java.time.ZoneId.of;
+import static java.util.Optional.empty;
+import static org.openmhealth.shim.common.mapper.JsonNodeMappingSupport.asOptionalInteger;
import static org.openmhealth.shim.common.mapper.JsonNodeMappingSupport.asOptionalLong;
-import static org.openmhealth.shim.common.mapper.JsonNodeMappingSupport.asRequiredLong;
/**
- * A mapper from Withings Sleep Summary endpoint responses (/sleep?action=getsummary) to {@link SleepDuration1}
+ * A mapper from Withings Sleep Summary endpoint responses (/sleep?action=getsummary) to {@link SleepDuration2}
* objects.
*
* @author Chris Schaefbauer
* @see Sleep Summary API documentation
*/
-public class WithingsSleepDurationDataPointMapper extends WithingsListDataPointMapper {
+public class WithingsSleepDurationDataPointMapper extends WithingsListDataPointMapper {
+
+ private WithingsSleepEpisodeDataPointMapper sleepEpisodeMapper = new WithingsSleepEpisodeDataPointMapper();
+
+ @Override
+ String getListNodeName() {
+ return "series";
+ }
/**
* Maps an individual list node from the array in the Withings sleep summary endpoint response into a {@link
- * SleepDuration1} data point.
+ * SleepDuration2} data point.
*
* @param node activity node from the array "series" contained in the "body" of the endpoint response
- * @return a {@link DataPoint} object containing a {@link SleepDuration1} measure with the appropriate values from
- * the JSON node parameter, wrapped as an {@link Optional}
+ * @return a {@link SleepDuration2} data point
*/
@Override
- Optional> asDataPoint(JsonNode node) {
-
- Long lightSleepInSeconds = asRequiredLong(node, "data.lightsleepduration");
- Long deepSleepInSeconds = asRequiredLong(node, "data.deepsleepduration");
- Long remSleepInSeconds = asRequiredLong(node, "data.remsleepduration");
+ Optional> asDataPoint(JsonNode node) {
- Long totalSleepInSeconds = lightSleepInSeconds + deepSleepInSeconds + remSleepInSeconds;
+ Optional> sleepEpisodeDataPoint = sleepEpisodeMapper.asDataPoint(node);
- SleepDuration1.Builder sleepDurationBuilder =
- new SleepDuration1.Builder(new DurationUnitValue(DurationUnit.SECOND, totalSleepInSeconds));
-
-
- Optional startDateInEpochSeconds = asOptionalLong(node, "startdate");
- Optional endDateInEpochSeconds = asOptionalLong(node, "enddate");
-
- if (startDateInEpochSeconds.isPresent() && endDateInEpochSeconds.isPresent()) {
- OffsetDateTime offsetStartDateTime =
- OffsetDateTime.ofInstant(Instant.ofEpochSecond(startDateInEpochSeconds.get()), of("Z"));
- OffsetDateTime offsetEndDateTime =
- OffsetDateTime.ofInstant(Instant.ofEpochSecond(endDateInEpochSeconds.get()), of("Z"));
- sleepDurationBuilder.setEffectiveTimeFrame(
- TimeInterval.ofStartDateTimeAndEndDateTime(offsetStartDateTime, offsetEndDateTime));
+ if (!sleepEpisodeDataPoint.isPresent()) {
+ return empty();
}
- Optional externalId = asOptionalLong(node, "id");
- Optional modelId = asOptionalLong(node, "model");
- String modelName = null;
+ SleepEpisode sleepEpisode = sleepEpisodeDataPoint.get().getBody();
- if (modelId.isPresent()) {
- modelName = SleepDeviceTypes.valueOf(modelId.get());
- }
+ SleepDuration2 sleepDuration =
+ new SleepDuration2.Builder(sleepEpisode.getTotalSleepTime(), sleepEpisode.getEffectiveTimeFrame())
+ .build();
- SleepDuration1 sleepDuration = sleepDurationBuilder.build();
- Optional wakeupCount = asOptionalLong(node, "data.wakeupcount");
- if (wakeupCount.isPresent()) {
- sleepDuration.setAdditionalProperty("wakeup_count", new Integer(wakeupCount.get().intValue()));
- }
+ sleepEpisode.getAdditionalProperties().forEach(sleepDuration::setAdditionalProperty);
- // These sleep phase values are Withings platform-specific, so we pass them through as additionalProperties to
- // ensure we keep relevant platform specific values. Should be interpreted according to Withings API spec
- sleepDuration.setAdditionalProperty("light_sleep_duration",
- new DurationUnitValue(DurationUnit.SECOND, lightSleepInSeconds));
- sleepDuration.setAdditionalProperty("deep_sleep_duration",
- new DurationUnitValue(DurationUnit.SECOND, deepSleepInSeconds));
- sleepDuration.setAdditionalProperty("rem_sleep_duration",
- new DurationUnitValue(DurationUnit.SECOND, remSleepInSeconds));
-
- // This is an additional piece of information captured by Withings devices around sleep and should be
- // interpreted according to the Withings API specification. We do not capture durationtowakeup or
- // wakeupduration properties from the Withings API because it is unclear the distinction between them and we
- // aim to avoid creating more ambiguity through passing through these properties
- Optional timeToSleepValue = asOptionalLong(node, "data.durationtosleep");
- if (timeToSleepValue.isPresent()) {
- sleepDuration.setAdditionalProperty("duration_to_sleep",
- new DurationUnitValue(DurationUnit.SECOND, timeToSleepValue.get()));
- }
+ Optional externalId = asOptionalLong(node, "id").map(Object::toString);
- return Optional.of(newDataPoint(sleepDuration, externalId.orElse(null), true, modelName));
- }
+ WithingsDevice device = asOptionalInteger(node, "model")
+ .flatMap(WithingsDevice::findByMagicNumber)
+ .orElse(null);
- @Override
- String getListNodeName() {
- return "series";
- }
-
- // TODO clean this up
- public enum SleepDeviceTypes {
- Pulse(16), Aura(32);
-
- private long deviceId;
-
- private static Map map = new HashMap();
-
- static {
- for (SleepDeviceTypes sleepDeviceTypeName : SleepDeviceTypes.values()) {
- map.put(sleepDeviceTypeName.deviceId, sleepDeviceTypeName.name());
- }
- }
-
- SleepDeviceTypes(final long deviceId) {
- this.deviceId = deviceId;
- }
-
- /**
- * Returns the string device name for a device ID
- *
- * @param deviceId the id number for the device contained within the Withings API response datapoint
- * @return common name of the device (e.g., Pulse, Aura)
- */
- public static String valueOf(long deviceId) {
- return map.get(deviceId);
- }
+ return Optional.of(newDataPoint(sleepDuration, externalId.orElse(null), true, device));
}
}
diff --git a/shim-server/src/main/java/org/openmhealth/shim/withings/mapper/WithingsSleepEpisodeDataPointMapper.java b/shim-server/src/main/java/org/openmhealth/shim/withings/mapper/WithingsSleepEpisodeDataPointMapper.java
new file mode 100644
index 00000000..90d3acb5
--- /dev/null
+++ b/shim-server/src/main/java/org/openmhealth/shim/withings/mapper/WithingsSleepEpisodeDataPointMapper.java
@@ -0,0 +1,101 @@
+/*
+ * Copyright 2017 Open mHealth
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.openmhealth.shim.withings.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.SleepEpisode;
+
+import java.time.OffsetDateTime;
+import java.util.Optional;
+
+import static org.openmhealth.schema.domain.omh.DurationUnit.SECOND;
+import static org.openmhealth.schema.domain.omh.TimeInterval.ofStartDateTimeAndEndDateTime;
+import static org.openmhealth.shim.common.mapper.JsonNodeMappingSupport.*;
+
+
+/**
+ * A mapper from Withings Sleep Summary endpoint responses (/sleep?action=getsummary) to {@link SleepEpisode} objects.
+ *
+ * @author Emerson Farrugia
+ * @author Chris Schaefbauer
+ * @see Sleep Summary API documentation
+ */
+public class WithingsSleepEpisodeDataPointMapper extends WithingsListDataPointMapper {
+
+ @Override
+ String getListNodeName() {
+ return "series";
+ }
+
+ /**
+ * Maps an individual list node from the array in the Withings sleep summary endpoint response into a {@link
+ * SleepEpisode} data point.
+ *
+ * @param node activity node from the array "series" contained in the "body" of the endpoint response
+ * @return a {@link SleepEpisode} data point
+ */
+ @Override
+ Optional> asDataPoint(JsonNode node) {
+
+ Long latencyToSleepOnsetInSeconds = asOptionalLong(node, "data.durationtosleep").orElse(0L);
+ Long latencyToArisingInSeconds = asOptionalLong(node, "data.durationtowakeup").orElse(0L);
+ String timeZoneId = asRequiredString(node, "timezone");
+
+ OffsetDateTime effectiveStartDateTime = asOffsetDateTime(asRequiredLong(node, "startdate"), timeZoneId)
+ .plusSeconds(latencyToSleepOnsetInSeconds);
+
+ OffsetDateTime effectiveEndDateTime = asOffsetDateTime(asRequiredLong(node, "enddate"), timeZoneId)
+ .minusSeconds(latencyToArisingInSeconds);
+
+ SleepEpisode.Builder sleepEpisodeBuilder =
+ new SleepEpisode.Builder(ofStartDateTimeAndEndDateTime(effectiveStartDateTime, effectiveEndDateTime))
+ .setLatencyToSleepOnset(new DurationUnitValue(SECOND, latencyToSleepOnsetInSeconds))
+ .setLatencyToArising(new DurationUnitValue(SECOND, latencyToArisingInSeconds));
+
+ Long lightSleepDurationInSeconds = asOptionalLong(node, "data.lightsleepduration").orElse(0L);
+ Long deepSleepDurationInSeconds = asOptionalLong(node, "data.deepsleepduration").orElse(0L);
+ Long remSleepDurationInSeconds = asOptionalLong(node, "data.remsleepduration").orElse(0L);
+
+ Long totalSleepDurationInSeconds =
+ lightSleepDurationInSeconds + deepSleepDurationInSeconds + remSleepDurationInSeconds;
+
+ sleepEpisodeBuilder.setTotalSleepTime(new DurationUnitValue(SECOND, totalSleepDurationInSeconds));
+
+ asOptionalInteger(node, "data.wakeupcount").ifPresent(sleepEpisodeBuilder::setNumberOfAwakenings);
+
+ SleepEpisode sleepEpisode = sleepEpisodeBuilder.build();
+
+ // These sleep phase values are Withings platform-specific, so we pass them through as additionalProperties to
+ // ensure we keep relevant platform specific values.
+ sleepEpisode.setAdditionalProperty("light_sleep_duration",
+ new DurationUnitValue(SECOND, lightSleepDurationInSeconds));
+ sleepEpisode.setAdditionalProperty("deep_sleep_duration",
+ new DurationUnitValue(SECOND, deepSleepDurationInSeconds));
+ sleepEpisode.setAdditionalProperty("rem_sleep_duration",
+ new DurationUnitValue(SECOND, remSleepDurationInSeconds));
+
+ Optional externalId = asOptionalLong(node, "id").map(Object::toString);
+
+ WithingsDevice device = asOptionalInteger(node, "model")
+ .flatMap(WithingsDevice::findByMagicNumber)
+ .orElse(null);
+
+ return Optional.of(newDataPoint(sleepEpisode, externalId.orElse(null), true, device));
+ }
+}
diff --git a/shim-server/src/main/resources/application.yaml b/shim-server/src/main/resources/application.yaml
index 118a60e2..64b3a13f 100644
--- a/shim-server/src/main/resources/application.yaml
+++ b/shim-server/src/main/resources/application.yaml
@@ -56,6 +56,8 @@ openmhealth:
# moves:
# client-id: "set-value-here"
# client-secret: "set-value-here"
+ # # whether to use app-based (moves://) or browser-based (http://) authorization flows
+ # authorization-initiated-from-browser: true
# runkeeper:
# client-id: "set-value-here"
# client-secret: "set-value-here"
diff --git a/shim-server/src/test/java/org/openmhealth/shim/fitbit/mapper/FitbitSleepDurationDataPointMapperUnitTests.java b/shim-server/src/test/java/org/openmhealth/shim/fitbit/mapper/FitbitSleepDurationDataPointMapperUnitTests.java
index aa121b8b..9a8e0c3e 100644
--- a/shim-server/src/test/java/org/openmhealth/shim/fitbit/mapper/FitbitSleepDurationDataPointMapperUnitTests.java
+++ b/shim-server/src/test/java/org/openmhealth/shim/fitbit/mapper/FitbitSleepDurationDataPointMapperUnitTests.java
@@ -1,3 +1,19 @@
+/*
+ * Copyright 2017 Open mHealth
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
package org.openmhealth.shim.fitbit.mapper;
import org.openmhealth.schema.domain.omh.DataPoint;
@@ -22,6 +38,7 @@ public class FitbitSleepDurationDataPointMapperUnitTests extends FitbitSleepMeas
private final FitbitSleepDurationDataPointMapper mapper = new FitbitSleepDurationDataPointMapper();
+ @Override
public FitbitSleepDurationDataPointMapper getMapper() {
return mapper;
}
diff --git a/shim-server/src/test/java/org/openmhealth/shim/fitbit/mapper/FitbitSleepEpisodeDataPointMapperUnitTests.java b/shim-server/src/test/java/org/openmhealth/shim/fitbit/mapper/FitbitSleepEpisodeDataPointMapperUnitTests.java
index 3d51275b..0c4e29e3 100644
--- a/shim-server/src/test/java/org/openmhealth/shim/fitbit/mapper/FitbitSleepEpisodeDataPointMapperUnitTests.java
+++ b/shim-server/src/test/java/org/openmhealth/shim/fitbit/mapper/FitbitSleepEpisodeDataPointMapperUnitTests.java
@@ -1,3 +1,19 @@
+/*
+ * Copyright 2017 Open mHealth
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
package org.openmhealth.shim.fitbit.mapper;
import org.openmhealth.schema.domain.omh.*;
@@ -20,6 +36,7 @@ public class FitbitSleepEpisodeDataPointMapperUnitTests extends FitbitSleepMeasu
private final FitbitSleepEpisodeDataPointMapper mapper = new FitbitSleepEpisodeDataPointMapper();
+ @Override
public FitbitSleepEpisodeDataPointMapper getMapper() {
return mapper;
}
diff --git a/shim-server/src/test/java/org/openmhealth/shim/fitbit/mapper/FitbitSleepMeasureDataPointMapperUnitTests.java b/shim-server/src/test/java/org/openmhealth/shim/fitbit/mapper/FitbitSleepMeasureDataPointMapperUnitTests.java
index 58531f10..ff6a77fb 100644
--- a/shim-server/src/test/java/org/openmhealth/shim/fitbit/mapper/FitbitSleepMeasureDataPointMapperUnitTests.java
+++ b/shim-server/src/test/java/org/openmhealth/shim/fitbit/mapper/FitbitSleepMeasureDataPointMapperUnitTests.java
@@ -1,3 +1,19 @@
+/*
+ * Copyright 2017 Open mHealth
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
package org.openmhealth.shim.fitbit.mapper;
import com.fasterxml.jackson.databind.JsonNode;
@@ -14,6 +30,9 @@
import static org.hamcrest.Matchers.empty;
+/**
+ * @author Emerson Farrugia
+ */
public abstract class FitbitSleepMeasureDataPointMapperUnitTests
extends DataPointMapperUnitTests {
diff --git a/shim-server/src/test/java/org/openmhealth/shim/googlefit/common/GoogleFitTestProperties.java b/shim-server/src/test/java/org/openmhealth/shim/googlefit/common/GoogleFitTestProperties.java
index 739ba8a8..f3b48502 100644
--- a/shim-server/src/test/java/org/openmhealth/shim/googlefit/common/GoogleFitTestProperties.java
+++ b/shim-server/src/test/java/org/openmhealth/shim/googlefit/common/GoogleFitTestProperties.java
@@ -18,32 +18,45 @@
import org.openmhealth.schema.domain.omh.DataPointModality;
import org.openmhealth.schema.domain.omh.SchemaId;
+import org.openmhealth.schema.domain.omh.TimeFrame;
+import java.time.OffsetDateTime;
+import java.util.ArrayList;
+import java.util.List;
import java.util.Optional;
+import static java.util.Optional.empty;
+import static org.openmhealth.schema.domain.omh.TimeInterval.ofStartDateTimeAndEndDateTime;
+
/**
* @author Chris Schaefbauer
+ * @author Emerson Farrugia
*/
public class GoogleFitTestProperties {
- private String startDateTime;
- private String endDateTime;
private String sourceOriginId;
- private double fpValue;
- private DataPointModality modality;
- private String stringValue;
- private long intValue;
private SchemaId bodySchemaId;
+ private DataPointModality modality;
+ private List