diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 00000000..aa5136ff --- /dev/null +++ b/.travis.yml @@ -0,0 +1,14 @@ +language: java +jdk: +- oraclejdk8 + +notifications: + slack: + rooms: + secure: F+o9FPgJxD6KnpcvCGm2VgvY72aCo/+RwBoIZoArB68L41rgiOUoKbqkH3Y1vLaqw6isyqa6dkuBKyFoKVwyjuDYn7Iaw2z86WZWRCB4SkqcAnVWQCUGZVPrFh38hUl++gUEYbzIPDLm8RgKOJr/9XBZaMoATJlmoenI9ZMuncI= +sudo: false + +cache: + directories: + - $HOME/.gradle/wrapper/dists/gradle-2.6-bin + - $HOME/.gradle/caches/modules-2/files-2.1 \ No newline at end of file diff --git a/README.md b/README.md index d2c6820e..6af6633b 100644 --- a/README.md +++ b/README.md @@ -1,246 +1,266 @@ -## Shimmer and shims +# Shimmer [![Build Status](https://travis-ci.org/openmhealth/shimmer.svg?branch=develop)](https://travis-ci.org/openmhealth/shimmer) -### Overview +Shimmer is an application that makes it easy to pull health data from popular third-party APIs like Runkeeper and Fitbit. +It converts that data into an [Open mHealth](http://www.openmhealth.org) compliant format, letting your application work with clean and clinically meaningful data. -A *shim* is an adapter that reads raw data from a third-party API (e.g. Jawbone, Fitbit) and converts that data into an [Open mHealth compliant data format](http://www.openmhealth.org/documentation/#/schema-docs/overview). It's called a shim -because it lets you treat third-party data like Open mHealth compliant data when writing your application. -To learn more about shims, please visit the [shim section](http://www.openmhealth.org/documentation/#/data-providers/data-provider-api-library) on our site. - -A shim is a library, not an application. To use a shim, it needs to be hosted in a standalone application called a *shim server*. -The shim server API lets your application use a shim to read data from a third-party API. This data is available in two formats; - the raw format produced by the third-party API and the converted Open mHealth compliant format. To make it easier to use the shim - server, we've provided a *shim server UI* that can trigger authentication flows and make requests. - -This repository contains a shim server, a shim server UI, and shims for third-party APIs. The currently supported APIs are: +We currently support the following APIs -* [Fat Secret](http://platform.fatsecret.com/api/) -* [Fitbit](http://dev.fitbit.com/) -* [Microsoft HealthVault](https://developer.healthvault.com/) -* [Jawbone UP](https://jawbone.com/up/developer) -* [RunKeeper](http://developer.runkeeper.com/healthgraph) ([application management portal](http://runkeeper.com/partner)) -* [Withings](http://oauth.withings.com/api) +* [Fitbit](https://www.fitbit.com/) +* [Google Fit](https://developers.google.com/fit/?hl=en) +* [Jawbone UP](https://jawbone.com/up) +* [Misfit](http://misfit.com/) +* [RunKeeper](https://runkeeper.com/index) +* [Withings](http://www.withings.com/) -The above links point to the developer website of each API. You'll need to visit these websites to register your -application and obtain authentication credentials for each of the shims you want to enable. +And the following APIs are in the works -If any of links are incorrect or out of date, please [submit an issue](https://github.com/openmhealth/omh-shims/issues) to let us know. +* [FatSecret](https://www.fatsecret.com/) +* [Ginsberg](https://www.ginsberg.io/) +* [iHealth](http://www.ihealthlabs.com/) +* [Strava](https://www.strava.com/) -Please note that the shim server is meant as a discovery and experimentation tool. It has not been secured and does not -attempt to protect the data retrieved from third-party APIs. +This README should have everything you need to get started. If you have any questions, feel free to [open an issue](https://github.com/openmhealth/shimmer/issues), [email us](mailto://admin@openmhealth.org), [post on our form](https://groups.google.com/forum/#!forum/omh-developers), or [visit our website](http://www.openmhealth.org/documentation/#/data-providers/get-started). +## Contents +- [Overview](#overview) + - [Shims](#shims) + - [Resource server](#resource-server) + - [Console](#console) +- [Installation](#installation) + - [Option 1. Download and run Docker images](#option-1-download-and-run-docker-images) + - [Option 2. Build the code and run it natively or in Docker](#option-2-build-the-code-and-run-it-natively-or-in-docker) +- [Setting up your credentials](#setting-up-your-credentials) +- [Authorizing access to a third-party user account](#authorizing-access-to-a-third-party-user-account) + - [Authorize access from the console](#authorize-access-from-the-console) + - [Authorize access programmatically](#authorize-access-programmatically) +- [Reading data](#reading-data) + - [Read data using the console](#read-data-using-the-console) + - [Read data programmatically](#read-data-programmatically) +- [Supported APIs and endpoints](#supported-apis-and-endpoints) +- [Contributing](#contributing) -### Installation +## Overview +Shimmer is made up of different components - individual shims, a resource server, and a console - which are each described below. -There are three ways to build and run the shim server. +### Shims +A *shim* is a library that can communicate with a specific third-party API, e.g. Withings. It handles the process of authenticating with the API, requesting data from it, and mapping that data into an Open mHealth compliant data format. -1. You can run a Docker container that executes a pre-built binary. - * This is the fastest way to get up and running and isolates the install from your system. -1. You can build all the code from source and run it on your host system. - * This is a quick way to get up and running - if your system already has MongoDB and is prepped to build Java code. -1. You can run a Docker container that builds all the code from source and executes the resulting binary. - * This isolates the install from your system while still letting you hack the code. But it can take a while to build - the container due to the large number of libraries and subsystems that need to be downloaded and installed. - * If you know Docker and want to speed things up, remove optional components from the Dockerfile and link them - from separate containers. +A shim generates *data points*, which are self-contained pieces of data that not only contain the health data of interest, but also include header information such as date of creation, acquisition provenance, and data source. This metadata helps describe the data and where it came from. The library is called a shim because such clean and clinically significant data in not provided natively by the third-party API. -#### Option 1. Running a pre-built binary in Docker +### Resource server +The *resource server* exposes an API to retrieve data points. The server handles API requests by delegating them to the correct shim. As more and more shims are developed and added to the resource server, it becomes capable of providing data points from more and more third-party APIs. The resource server also manages third-party access tokens on behalf of shims. -If you don't have Docker installed, download [Docker](https://docs.docker.com/installation/#installation/) - and follow the installation instructions for your platform. +### Console +The *console* provides a simple web interface that helps users interact with the resource server. It can set configuration parameters, trigger authentication flows, and request data using date pickers and drop downs. -Then in a terminal, +## Installation -1. If you don't already have a MongoDB container, download one by running - * `docker pull mongo:latest` - * Note that this will download around 400 MB of Docker images. -1. If your MongoDB container isn't running, start it by running - * `docker run --name some-mongo -d mongo:latest` -1. Download the shim server image by running - * `docker pull openmhealth/omh-shim-server:latest` - * Note that this will download up to 600MB of Docker images. (203MB for Ubuntu, 350MB for the OpenJDK 7 JRE, and 30MB - for the shim server and its dependencies.) -1. Start the shim server by running - * `docker run -e openmhealth.shim.server.callbackUrlBase=http://:8083 --link some-mongo:mongo -d -p 8083:8083 'openmhealth/omh-shim-server:latest'` -1. The server should now be running on the Docker host on default port 8083. You can change the port number in the Docker `run` command. -1. Visit `http://:8083` in a browser. +There are two ways to install Shimmer. -#### Option 2. Building from source and running on your host system +1. You can download and run pre-built Docker images. +1. You can build all the code from source and run it natively or in Docker. -If you prefer not to use Docker, +### Option 1. Download and run Docker images -1. You must have a Java 7 or higher JDK installed. You can use either [OpenJDK](http://openjdk.java.net/install/) or the [Oracle JDK](http://www.oracle.com/technetwork/java/javase/downloads/index.html). -1. A running [MongoDB](http://docs.mongodb.org/manual/) installation is required. -1. [Maven](http://maven.apache.org/) is required to build and install Microsoft HealthVault libraries. -1. You technically don't need to run the shim server UI, but it makes your life easier. If you're building the UI, - 1. [Node.js](http://nodejs.org/download/) is required. - 1. [Xcode Command Line Tools](https://developer.apple.com/xcode/) are required if you're on a Mac. +If you don't have Docker, Docker Compose, and Docker Machine installed, download [Docker Toolbox](https://www.docker.com/toolbox) and follow the installation instructions for your platform. -Then, +Once you have a running Docker host, in a terminal 1. Clone this Git repository. -1. Navigate to the `shim-server-ui` directory in a terminal and run - 1. `npm install` - 1. `sudo npm install -g grunt-cli bower` - 1. `bower install` - 1. `grunt build` -1. Navigate to the `shim-server/src/main/resources` directory and run - 1. `ln -s ../../../../shim-server-ui/dist public` -1. Edit the `application.yaml` file. - * Check that the `spring:data:mongodb:uri` parameter points to your running MongoDB instance. - * You might need to change the host to `localhost`, for example. -1. Follow [these instructions](#preparing-to-use-microsoft-healthvault) to install Microsoft HealthVault libraries. These libraries are - currently required for the shim server to work. -1. To build and run the shim server, navigate to the base directory and run `./gradlew shim-server:bootRun` -1. The server should now be running on `localhost` on port 8083. You can change the port number in the `application.yaml` file. +1. Run `docker-machine ls` to find the name and IP address of your active Docker host. +1. Run `eval "$(docker-machine env host)"` to prepare environment variables, *replacing `host` with the name of your Docker host*. +1. Run the `./update-compose-files.sh` script. + * This step should be removed once Compose 1.5 is released. +1. Download and start the containers by running + * `docker-compose up -d` + * If you want to see logs and keep the containers in the foreground, omit the `-d`. + * This will download up to 1 GB of Docker images if you don't already have them, the bulk of which are MongoDB, nginx and OpenJDK base images. + * It can take up to a minute for the containers to start up. You can check their progress using `docker-compose logs` if you started with `-d`. +1. Visit `http://:8083` in a browser. + +### Option 2. Build the code and run it natively or in Docker + +If you prefer to build the code yourself, + +1. You must have a Java 8 or higher JDK installed. You can use either [OpenJDK](http://openjdk.java.net/install/) or the [Oracle JDK](http://www.oracle.com/technetwork/java/javase/downloads/index.html). +1. You technically don't need to run the console, but it makes your life easier. If you're building the console, + 1. You need [Node.js](http://nodejs.org/download/). + 1. You need [Xcode Command Line Tools](https://developer.apple.com/xcode/) if you're on a Mac. +1. To run the code natively, + 1. You need a running [MongoDB](http://docs.mongodb.org/manual/) instance. +1. To run the code in Docker, + 1. You need Docker, Docker Compose, and Docker Machine, available in [Docker Toolbox](https://www.docker.com/toolbox). + 1. You need a running Docker host. + +If you want to build and run the code natively, in a terminal + +1. Clone this Git repository. +1. Run the `./run-natively.sh` script and follow the instructions. +1. When the script blocks with the message `Started Application`, the components are running. + * Press Ctrl-C to stop them. 1. Visit `http://localhost:8083` in a browser. - -##### Preparing to use Microsoft HealthVault - -The Microsoft HealthVault shim has dependencies which can't be automatically downloaded from public servers, at least -not yet. To add HealthVault support to the shim server, - -1. Download the HealthVault Java Library version [R1.6.0](https://healthvaultjavalib.codeplex.com/releases/view/125355) archive. -1. Extract the archive. -1. Navigate to the extracted directory in a terminal. -1. Run `mvn install -N && mvn install --pl sdk,hv-jaxb -DskipTests` - -This will make the HealthVault libraries available to Gradle. - -#### Option 3. Building from source and running in Docker - -If you don't have Docker installed, download [Docker](https://docs.docker.com/installation/#installation/) - and follow the installation instructions for your platform. - -Then, - -1. Download the latest [release](https://github.com/openmhealth/omh-shims/releases) of this Git repository or clone it. -1. Navigate to the `docker/source` directory in a terminal. -1. Run `docker build -t="openmhealth/omh-shim-server" .` - * This will require about 1.5GB of disk space. -1. Run `docker run -d -p 8083:8083 -p 2022:22 openmhealth/omh-shim-server` -1. The server should now be running on the Docker host on default port 8083. You can change the port number in the Docker `run` command. + +If you want to build and run the code in Docker, in a terminal + +1. Clone this Git repository. +1. Run `docker-machine ls` to find the name of your active Docker host. +1. Run `eval "$(docker-machine env host)"` to prepare environment variables, *replacing `host` with the name of your Docker host*. +1. Run the `./run-dockerized.sh` script and follow the instructions. + * The containers should now be running on your Docker host and expose port 8083. + * It can take up to a minute for the containers to start up. 1. Visit `http://:8083` in a browser. -If you want to SSH into the container, run `ssh root@ -p 2022`. The password is `docker`. +> If you can't run the Bash scripts on your system, open them and take a look at the commands they run. The important commands are marked with a "#CMD" comment. -### Setting up your credentials +## Setting up your credentials -You need to obtain authentication credentials, typically an OAuth client ID and client secret, for any shim you'd like to run. -These are obtained from the developer websites of the third-party APIs. +You need to obtain client credentials for any shim you'd like to use. These credentials are typically an OAuth client ID and client secret, and you can generate them from the developer website of the third-party API. Visit the following links to register your application and obtain authentication credentials for each of the shims you want to use. -Once credentials are obtained for a particular API, navigate to the settings tab of the shim server UI and fill them in. +* [Fitbit](http://dev.fitbit.com/) +* [Google Fit](https://developers.google.com/fit/rest/) ([application management portal](https://console.developers.google.com/start)) +* [Jawbone UP](https://jawbone.com/up/developer) +* [Misfit](https://build.misfit.com/) +* [RunKeeper](http://developer.runkeeper.com/healthgraph) ([application management portal](http://runkeeper.com/partner)) +* [Withings](http://oauth.withings.com/api) + +If any of the links are incorrect or out of date, please [submit an issue](https://github.com/openmhealth/shimmer/issues) to let us know. + +Once credentials are obtained for a particular API, navigate to the settings tab of the console and fill them in. -(If you didn't build the UI, uncomment and replace the corresponding `clientId` and `clientSecret` placeholders in the `application.yaml` file -with your new credentials and restart Jetty. If you installed using Docker, you can restart Jetty using `supervisorctl restart jetty`. -If you installed manually, terminate your running Gradle process and restart it.) +> If you didn't build the console, uncomment and replace the corresponding `clientId` and `clientSecret` placeholders in the `application.yaml` file +with your new credentials and rebuild. -### Authorising access to a third-party user account from the UI +## Authorizing access to a third-party user account The data produced by a third-party API belongs to some user account registered on the third-party system. To allow - a shim to read that data, you'll need to initiate an authorization process that lets the account holder grant the shim access to their data. + a shim to read that data, you'll need to initiate an authorization process. This process lets the user account holder explicitly grant the shim access to their data. -To initiate the authorization process from the UI, +### Authorize access from the console + +To initiate the authorization process from the console, 1. Type in an arbitrary user handle. This handle can be anything, it's just your way of referring to third-party API users. -1. Press *Find* and the UI will show you a *Connect* button for each API whose authentication credentials have been [configured](#setting-up-your-credentials). +1. Press *Find* and the console will show you a *Connect* button for each API with [configured](#setting-up-your-credentials) authentication credentials. 1. Click *Connect* and a pop-up will open. 1. Follow the authorization prompts. You should see an `AUTHORIZE` JSON response. 1. Close the pop-up. -### Authorising access to a third-party user account programmatically +### Authorize access programmatically To initiate the authorization process programmatically, 1. Make a GET request to `http://:8083/authorize/{shim}?username={userId}` * The `shim` path parameter should be one of the names listed [below](#supported-apis-and-endpoints), e.g. `fitbit`. * The `username` query parameter can be set to any unique identifier you'd like to use to identify the user. -1. In the returned JSON response, find the `authorizationUrl` value and redirect your user to this URL. Your user will land on the third-party website where they can login and authorize access to their third-party user account. +1. Find the `authorizationUrl` value in the returned JSON response and redirect your user to this URL. Your user will land on the third-party website where they can login and authorize access to their third-party user account. 1. Once authorized, they will be redirected to `http://:8083/authorize/{shim_name}/callback` along with an approval response. -### Reading data using the UI - -To pull data from the third-party API using the UI, +## Reading data +A shim can produce JSON data that is either *normalized* to Open mHealth schemas or in the *raw* format produced by the third-party API. Raw data is passed through from the third-party API. Normalized data conforms to [Open mHealth schemas](http://www.openmhealth.org/documentation/#/schema-docs/schema-library). + +The following is an example of a normalized step count data point retrieved from Jawbone: + +```json +{ + "header": { + "id": "243c773b-8936-407e-9c23-270d0ea49cc4", + "creation_date_time": "2015-09-10T12:43:39.138-06:00", + "acquisition_provenance": { + "source_name": "Jawbone UP API", + "modality": "sensed", + "source_updated_date_time": "2015-09-10T18:43:39Z" + }, + "schema_id": { + "namespace": "omh", + "name": "step-count", + "version": "1.0" + } + }, + "body": { + "effective_time_frame": { + "time_interval": { + "start_date_time": "2015-08-06T05:11:09-07:00", + "end_date_time": "2015-08-06T23:00:36-06:00" + } + }, + "step_count": 7939 + } +} +``` + +### Read data using the console + +To pull data from a third-party API using the console, -1. Click the nam of the connected third-party API. +1. Click the name of the connected third-party API. 1. Fill in the date range you're interested in. 1. Press the *Raw* button for raw data, or the *Normalized* button for data that has been converted to an Open mHealth compliant data format. -### Reading data programmatically +### Read data programmatically -To pull data from the third-party API programmatically, make requests in the format +To pull data from a third-party API programmatically, make requests in the format `http://:8083/data/{shim}/{endPoint}?username={userId}&dateStart=yyyy-MM-dd&dateEnd=yyyy-MM-dd&normalize={true|false}` The URL can be broken down as follows * The `shim` and `username` path variables are the same as [above](#authorizing-access-to-a-third-party-user-account). -* The `endPoint` path variable roughly corresponds to the type of data to retrieve. There's a list of these [below](#supported-apis-and-endpoints). +* The `endPoint` path variable corresponds to the type of data to retrieve. There's a table of these [below](#supported-apis-and-endpoints). * The `normalize` parameter controls whether the shim returns data in a raw third-party API format (`false`) or in an Open mHealth compliant format (`true`). - -### Supported APIs and endpoints -The following is a nested list in the format - -* shim name - * endpoint name - * Open mHealth compliant data the endpoint can produce - -The currently supported shims are +> N.B. This API will be changing significantly in the near future to provide greater consistency across Open mHealth applications and to improve expressivity and ease of use. The data points it returns will not be affected, only the URLs used to request data and perhaps some book-keeping information at the top level of the response. -* fitbit - * activity - * [omh:physical-activity](http://www.openmhealth.org/documentation/#/schema-docs/schema-library/schemas/omh_physical-activity) - * blood_pressure - * [omh:blood-pressure](http://www.openmhealth.org/documentation/#/schema-docs/schema-library/schemas/omh_blood-pressure) - * blood_glucose - * [omh:blood-glucose](http://www.openmhealth.org/documentation/#/schema-docs/schema-library/schemas/omh_blood-glucose) - * heart - * [omh:heart-rate](http://www.openmhealth.org/documentation/#/schema-docs/schema-library/schemas/omh_heart-rate) - * steps - * [omh:step-count](http://www.openmhealth.org/documentation/#/schema-docs/schema-library/schemas/omh_step-count) - * weight - * [omh:body-weight](http://www.openmhealth.org/documentation/#/schema-docs/schema-library/schemas/omh_body-weight) -* healthvault - * activity - * [omh:physical-activity](http://www.openmhealth.org/documentation/#/schema-docs/schema-library/schemas/omh_physical-activity) - * blood_glucose - * [omh:blood-glucose](http://www.openmhealth.org/documentation/#/schema-docs/schema-library/schemas/omh_blood-glucose) - * blood_pressure - * [omh:blood-pressure](http://www.openmhealth.org/documentation/#/schema-docs/schema-library/schemas/omh_blood-pressure) - * [omh:heart-rate](http://www.openmhealth.org/documentation/#/schema-docs/schema-library/schemas/omh_heart-rate) - * height - * [omh:body-height](http://www.openmhealth.org/documentation/#/schema-docs/schema-library/schemas/omh_body-height) - * weight - * [omh:body-weight](http://www.openmhealth.org/documentation/#/schema-docs/schema-library/schemas/omh_body-weight) -* jawbone - * body - * [omh:body-weight](http://www.openmhealth.org/documentation/#/schema-docs/schema-library/schemas/omh_body-weight) - * moves - * [omh:step-count](http://www.openmhealth.org/documentation/#/schema-docs/schema-library/schemas/omh_step-count) - * sleep - * [omh:sleep-duration](http://www.openmhealth.org/documentation/#/schema-docs/schema-library/schemas/omh_sleep-duration) - * workouts - * [omh:physical-activity](http://www.openmhealth.org/documentation/#/schema-docs/schema-library/schemas/omh_physical-activity) -* runkeeper - * activity - * [omh:physical-activity](http://www.openmhealth.org/documentation/#/schema-docs/schema-library/schemas/omh_physical-activity) - * weight - * [omh:body-weight](http://www.openmhealth.org/documentation/#/schema-docs/schema-library/schemas/omh_body-weight) -* withings - * body - * [omh:blood-pressure](http://www.openmhealth.org/documentation/#/schema-docs/schema-library/schemas/omh_blood-pressure) - * [omh:body-height](http://www.openmhealth.org/documentation/#/schema-docs/schema-library/schemas/omh_body-height) - * [omh:body-weight](http://www.openmhealth.org/documentation/#/schema-docs/schema-library/schemas/omh_body-weight) - * [omh:heart-rate](http://www.openmhealth.org/documentation/#/schema-docs/schema-library/schemas/omh_heart-rate) - * intraday - * [omh:step-count](http://www.openmhealth.org/documentation/#/schema-docs/schema-library/schemas/omh_step-count) - * sleep - * [omh:sleep-duration](http://www.openmhealth.org/documentation/#/schema-docs/schema-library/schemas/omh_sleep-duration) - -You can learn more about these shims and endpoints in the [documentation section](http://www.openmhealth.org/documentation/#/overview/get-started) of the Open mHealth site. - -The list of supported third-party APIs will grow over time as more shims are added. If you'd like to contribute a shim to work with your API or a third-party API, -send us a [pull request](https://github.com/openmhealth/shimmer/pulls). If you need any help, feel free to -reach out on [admin@openmhealth.org](mailto://admin@openmhealth.org) or on the Open mHealth [developer group](https://groups.google.com/forum/#!forum/omh-developers). - - +## Supported APIs and endpoints + +The following is a table of the currently supported shims, their endpoints, and the Open mHealth compliant data that each endpoint can produce. The values in the `shim` and `endPoint` columns are the values for the parameters of the same names used in [programmatic access](#reading-data-programmatically) of the API. + +The currently supported shims are: + +| shim | endPoint | OmH data produced by endpoint | +| ------------ | ----------------- | -------------------------- | +| fitbit1 | activity | [omh:physical-activity](http://www.openmhealth.org/documentation/#/schema-docs/schema-library/schemas/omh_physical-activity) | +| fitbit1 | steps | [omh:step-count](http://www.openmhealth.org/documentation/#/schema-docs/schema-library/schemas/omh_step-count) | +| fitbit1 | weight | [omh:body-weight](http://www.openmhealth.org/documentation/#/schema-docs/schema-library/schemas/omh_body-weight) | +| fitbit1 | body_mass_index | [omh:body-mass-index](http://www.openmhealth.org/documentation/#/schema-docs/schema-library/schemas/omh_body-mass-index)| +| fitbit1 | sleep | [omh:sleep-duration](http://www.openmhealth.org/documentation/#/schema-docs/schema-library/schemas/omh_sleep-duration) | +| googlefit | activity | [omh:physical-activity](http://www.openmhealth.org/documentation/#/schema-docs/schema-library/schemas/omh_physical-activity) | +| googlefit | body_height | [omh:body-height](http://www.openmhealth.org/documentation/#/schema-docs/schema-library/schemas/omh_body-height) | +| googlefit | body_weight | [omh:body-weight](http://www.openmhealth.org/documentation/#/schema-docs/schema-library/schemas/omh_body-weight) | +| googlefit | heart_rate | [omh:heart-rate](http://www.openmhealth.org/documentation/#/schema-docs/schema-library/schemas/omh_heart-rate) +| googlefit | step_count | [omh:step-count](http://www.openmhealth.org/documentation/#/schema-docs/schema-library/schemas/omh_step-count) +| googlefit | calories_burned | [omh:calories-burned](http://www.openmhealth.org/documentation/#/schema-docs/schema-library/schemas/omh_calories-burned) | +| jawbone | activity | [omh:physical-activity](http://www.openmhealth.org/documentation/#/schema-docs/schema-library/schemas/omh_physical-activity) | +| jawbone | weight | [omh:body-weight](http://www.openmhealth.org/documentation/#/schema-docs/schema-library/schemas/omh_body-weight) +| jawbone | body_mass_index | [omh:body-mass-index](http://www.openmhealth.org/documentation/#/schema-docs/schema-library/schemas/omh_body-mass-index) | +| jawbone | steps | [omh:step-count](http://www.openmhealth.org/documentation/#/schema-docs/schema-library/schemas/omh_step-count) | +| jawbone | sleep | [omh:sleep-duration](http://www.openmhealth.org/documentation/#/schema-docs/schema-library/schemas/omh_sleep-duration) | +| jawbone | heart_rate | [omh:heart-rate](http://www.openmhealth.org/documentation/#/schema-docs/schema-library/schemas/omh_heart-rate) | +| misift | activities | [omh:physical-activity](http://www.openmhealth.org/documentation/#/schema-docs/schema-library/schemas/omh_physical-activity) | +| misift | steps | [omh:step-count](http://www.openmhealth.org/documentation/#/schema-docs/schema-library/schemas/omh_step-count) +| misift | sleep | [omh:sleep-duration](http://www.openmhealth.org/documentation/#/schema-docs/schema-library/schemas/omh_sleep-duration) | +| runkeeper | activity | [omh:physical-activity](http://www.openmhealth.org/documentation/#/schema-docs/schema-library/schemas/omh_physical-activity) | +| runkeeper | calories | [omh:calories-burned](http://www.openmhealth.org/documentation/#/schema-docs/schema-library/schemas/omh_calories-burned) | +| withings | blood_pressure | [omh:blood-pressure](http://www.openmhealth.org/documentation/#/schema-docs/schema-library/schemas/omh_blood-pressure)| +| withings | body_height | [omh:body-height](http://www.openmhealth.org/documentation/#/schema-docs/schema-library/schemas/omh_body-height)| +| withings | body_weight | [omh:body-weight](http://www.openmhealth.org/documentation/#/schema-docs/schema-library/schemas/omh_body-weight) | +| withings | heart_rate | [omh:heart-rate](http://www.openmhealth.org/documentation/#/schema-docs/schema-library/schemas/omh_heart-rate) | +| withings | steps2 | [omh:step-count](http://www.openmhealth.org/documentation/#/schema-docs/schema-library/schemas/omh_step-count) | +| withings | calories2 | [omh:calories-burned](http://www.openmhealth.org/documentation/#/schema-docs/schema-library/schemas/omh_calories-burned) | +| withings | sleep3 | [omh:sleep-duration](http://www.openmhealth.org/documentation/#/schema-docs/schema-library/schemas/omh_sleep-duration) | + + +1 *The Fitbit API does not provide time zone information for the data points it returns. Furthermore, it is not possible to infer the time zone from any of the information provided. Because Open mHealth schemas require timestamps to have a time zone, we need to assign a time zone to timestamps. We set the time zone of all timestamps to UTC for consistency, even if the data may not have occurred in that time zone. This means that unless the event actually occurred in UTC, the timestamps will be incorrect. Please consider this when working with data normalized into OmH schemas that are retrieved from the Fitbit shim. We will fix this as soon as Fitbit makes changes to their API to provide time zone information.* + +2 *Uses the daily activity summary when partner access is disabled (default) and uses intraday activity when partner access is enabled. See the YAML configuration file for details. Intraday activity requests are limited to 24 hours worth of data per request.* + +3 *Sleep data has not been tested using real data directly from a device. It has been tested with example data provided in the Withings API documentation.* + +### Contributing + +The list of supported third-party APIs will grow over time as more shims are added. If you'd like to contribute a shim to work with your API or a third-party API, or contribute any other code, + +1. [Open an issue](https://github.com/openmhealth/shimmer/issues) to let us know what you're going to work on. + 1. This lets us give you feedback early and lets us put you in touch with people who can help. +2. Fork this repository. +3. Create your feature branch from the `develop` branch. +4. Commit and push your changes to your fork. +5. Create a pull request. diff --git a/build.gradle b/build.gradle index a1e480c9..c50d9b4d 100644 --- a/build.gradle +++ b/build.gradle @@ -3,21 +3,25 @@ subprojects { apply plugin: 'java' repositories { - mavenLocal() - mavenCentral() + jcenter() } group = 'org.openmhealth.shim' ext { - javaVersion = 1.7 - shimServerVersion = '0.2.3' + javaVersion = 1.8 + shimmerVersion = '0.3.0.SNAPSHOT' + omhSchemaSdkVersion = '1.0.3' } sourceCompatibility = javaVersion targetCompatibility = javaVersion + + test { + useTestNG() + } } task wrapper(type: Wrapper) { - gradleVersion = '2.2.1' + gradleVersion = '2.6' } diff --git a/docker-compose-build.yml b/docker-compose-build.yml new file mode 100644 index 00000000..59d1275f --- /dev/null +++ b/docker-compose-build.yml @@ -0,0 +1,17 @@ +resourceserver: + build: shim-server/docker + environment: + SPRING_PROFILES_ACTIVE: development + OPENMHEALTH_SHIM_SERVER_CALLBACKURLBASE: http://192.168.99.100:8083 + links: + - mongo:mongo + +mongo: + image: mongo + +console: + build: shim-server-ui/docker + ports: + - "8083:8083" + links: + - resourceserver:resource-server diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 00000000..9293467c --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,17 @@ +resourceserver: + image: openmhealth/shimmer-resource-server + environment: + SPRING_PROFILES_ACTIVE: development + OPENMHEALTH_SHIM_SERVER_CALLBACKURLBASE: http://192.168.99.100:8083 + links: + - mongo:mongo + +mongo: + image: mongo + +console: + image: openmhealth/shimmer-console + ports: + - "8083:8083" + links: + - resourceserver:resource-server diff --git a/docker/binary/Dockerfile b/docker/binary/Dockerfile deleted file mode 100644 index 0dfdeadf..00000000 --- a/docker/binary/Dockerfile +++ /dev/null @@ -1,9 +0,0 @@ -FROM openmhealth/openjdk-7-jre - -MAINTAINER Emerson Farrugia - -RUN mkdir /root/omh-shims -ADD shim-server.jar /root/omh-shims/ -EXPOSE 8083 - -CMD /usr/bin/java -jar /root/omh-shims/shim-server.jar diff --git a/docker/source/Dockerfile b/docker/source/Dockerfile deleted file mode 100644 index 6534fc83..00000000 --- a/docker/source/Dockerfile +++ /dev/null @@ -1,60 +0,0 @@ -FROM ubuntu:14.04 -# installed packages are listed at http://packages.ubuntu.com/trusty/ubuntu-minimal - -MAINTAINER Emerson Farrugia - -# update package lists -RUN apt-get update && apt-get -y upgrade - -# install build tools -RUN apt-get -y install git maven curl nodejs nodejs-legacy npm openjdk-7-jdk && apt-get clean -RUN update-alternatives --display java - -# install Mongo -# you can leave this section out and update supervisord.conf if you prefer to use a separate container for MongoDB -RUN apt-key adv --keyserver hkp://keyserver.ubuntu.com:80 --recv 7F0CEB10 -RUN echo 'deb http://downloads-distro.mongodb.org/repo/ubuntu-upstart dist 10gen' | tee /etc/apt/sources.list.d/10gen.list -RUN apt-get update && apt-get install -y mongodb-org -RUN mkdir -p /data/db - -# install OpenSSH -# you can leave this section out and update supervisord.conf if you prefer to not install OpenSSH in the container and -# access the container using other means, e.g. http://blog.docker.com/2014/06/why-you-dont-need-to-run-sshd-in-docker/ -RUN apt-get -y install openssh-server -RUN mkdir /var/run/sshd && echo 'root:docker' | chpasswd -RUN sed --in-place=.bak 's/without-password/yes/' /etc/ssh/sshd_config -EXPOSE 22 - -# install supervisord -# you can leave this section out if you've left out both Mongo and OpenSSH -RUN apt-get -y install supervisor -COPY supervisord.conf /etc/supervisor/conf.d/supervisord.conf - -# install the HealthVault Java library from https://healthvaultjavalib.codeplex.com/releases/view/125355 -# this library is currently necessary even if you don't intend to integrate with HealthVault -RUN curl -o /root/healthvault-sdk-1.6.0.zip 'http://download-codeplex.sec.s-msft.com/Download/Release?ProjectName=healthvaultjavalib&DownloadId=878881&FileTime=130496923011230000&Build=21018' -WORKDIR /root -RUN unzip healthvault-sdk-1.6.0.zip && cd healthvault-sdk-1.6.0 && mvn install -N && mvn install --pl sdk,hv-jaxb -DskipTests - -# clone the repository -WORKDIR /root -RUN git clone https://github.com/openmhealth/omh-shims.git -WORKDIR /root/omh-shims -RUN git checkout v0.2.1 - -# build the frontend -WORKDIR /root/omh-shims/shim-server-ui -RUN npm install -g grunt-cli bower -RUN npm install -RUN bower install --allow-root -RUN grunt build - -# build the backend -WORKDIR /root/omh-shims/shim-server -RUN ln -s dist ../shim-server/src/main/resources/public && mvn compile -EXPOSE 8083 - -CMD ["/usr/bin/supervisord"] - -# if you're not using supervisord, replace the above with -# CMD cd root/omh-shims && mvn spring-boot:run diff --git a/docker/source/supervisord.conf b/docker/source/supervisord.conf deleted file mode 100644 index e2971a20..00000000 --- a/docker/source/supervisord.conf +++ /dev/null @@ -1,12 +0,0 @@ -[supervisord] -nodaemon=true - -[program:sshd] -command=/usr/sbin/sshd -D - -[program:jetty] -directory=/root/omh-shims -command=mvn spring-boot:run - -[program:mongod] -command=/usr/bin/mongod diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index c97a8bdb..3d0dee6e 100644 Binary files a/gradle/wrapper/gradle-wrapper.jar and b/gradle/wrapper/gradle-wrapper.jar differ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 0f6bc409..e5982493 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ -#Fri Jan 09 15:27:10 CET 2015 +#Wed Sep 09 09:22:31 CEST 2015 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-2.2.1-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-2.6-bin.zip diff --git a/java-schema-objects/.gitignore b/java-schema-objects/.gitignore deleted file mode 100644 index 05958554..00000000 --- a/java-schema-objects/.gitignore +++ /dev/null @@ -1,3 +0,0 @@ -# rename that package from build to builder, if we even keep them -!src/main/java/org/openmhealth/schema/pojos/build -!src/test/java/org/openmhealth/schema/pojos/build \ No newline at end of file diff --git a/java-schema-objects/build.gradle b/java-schema-objects/build.gradle deleted file mode 100644 index 704e9d76..00000000 --- a/java-schema-objects/build.gradle +++ /dev/null @@ -1,17 +0,0 @@ -apply plugin: 'maven' - -group = 'org.openmhealth.schema' -version = '0.1' - -ext { - jacksonVersion = '2.4.4' -} - -dependencies { - compile "com.fasterxml.jackson.core:jackson-annotations:${jacksonVersion}" - compile "com.fasterxml.jackson.core:jackson-databind:${jacksonVersion}" - compile "joda-time:joda-time:2.4" - - testCompile "com.github.fge:json-schema-validator:2.2.6" - testCompile "junit:junit:4.11" -} diff --git a/java-schema-objects/src/main/java/org/openmhealth/schema/pojos/Activity.java b/java-schema-objects/src/main/java/org/openmhealth/schema/pojos/Activity.java deleted file mode 100644 index c8336448..00000000 --- a/java-schema-objects/src/main/java/org/openmhealth/schema/pojos/Activity.java +++ /dev/null @@ -1,98 +0,0 @@ -/* - * Copyright 2014 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.schema.pojos; - - -import com.fasterxml.jackson.annotation.JsonIgnore; -import com.fasterxml.jackson.annotation.JsonInclude; -import com.fasterxml.jackson.annotation.JsonProperty; -import com.fasterxml.jackson.annotation.JsonRootName; -import org.joda.time.DateTime; -import org.openmhealth.schema.pojos.generic.LengthUnitValue; -import org.openmhealth.schema.pojos.generic.TimeFrame; - - -/** - * @author Danilo Bonilla - */ -@JsonInclude(JsonInclude.Include.NON_NULL) -@JsonRootName(value = Activity.SCHEMA_ACTIVITY, namespace = DataPoint.NAMESPACE) -public class Activity extends BaseDataPoint { - - @JsonProperty(value = "activity_name", required = true) - private String activityName; - - @JsonProperty(value = "distance", required = false) - private LengthUnitValue distance; - - @JsonProperty(value = "reported_activity_intensity", required = false) - private ActivityIntensity reportedActivityIntensity; - - public enum ActivityIntensity {light, moderate, vigorous} - - @JsonProperty("effective_time_frame") - private TimeFrame effectiveTimeFrame; - - public static final String SCHEMA_ACTIVITY = "physical_activity"; - - public Activity() { - } - - @Override - @JsonIgnore - public String getSchemaName() { - return SCHEMA_ACTIVITY; - } - - @Override - @JsonIgnore - public DateTime getTimeStamp() { - return effectiveTimeFrame.getTimestamp(); - } - - public LengthUnitValue getDistance() { - return distance; - } - - public void setDistance(LengthUnitValue distance) { - this.distance = distance; - } - - public ActivityIntensity getReportedActivityIntensity() { - return reportedActivityIntensity; - } - - public void setReportedActivityIntensity(ActivityIntensity reportedActivityIntensity) { - this.reportedActivityIntensity = reportedActivityIntensity; - } - - public String getActivityName() { - return activityName; - } - - public void setActivityName(String activityName) { - this.activityName = activityName; - } - - public TimeFrame getEffectiveTimeFrame() { - return effectiveTimeFrame; - } - - public void setEffectiveTimeFrame(TimeFrame effectiveTimeFrame) { - this.effectiveTimeFrame = effectiveTimeFrame; - } -} diff --git a/java-schema-objects/src/main/java/org/openmhealth/schema/pojos/BaseDataPoint.java b/java-schema-objects/src/main/java/org/openmhealth/schema/pojos/BaseDataPoint.java deleted file mode 100644 index 196ae74f..00000000 --- a/java-schema-objects/src/main/java/org/openmhealth/schema/pojos/BaseDataPoint.java +++ /dev/null @@ -1,40 +0,0 @@ -/* - * Copyright 2014 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.schema.pojos; - -import com.fasterxml.jackson.annotation.JsonProperty; - - -/** - * Common code for data points, primarily related - * to metadata. - * - * @author Danilo Bonilla - */ -public abstract class BaseDataPoint implements DataPoint { - - @JsonProperty(value = "metadata") - protected Metadata metadata; - - public Metadata getMetadata() { - return metadata; - } - - public void setMetadata(Metadata metadata) { - this.metadata = metadata; - } -} diff --git a/java-schema-objects/src/main/java/org/openmhealth/schema/pojos/BloodGlucose.java b/java-schema-objects/src/main/java/org/openmhealth/schema/pojos/BloodGlucose.java deleted file mode 100644 index 2d697c65..00000000 --- a/java-schema-objects/src/main/java/org/openmhealth/schema/pojos/BloodGlucose.java +++ /dev/null @@ -1,140 +0,0 @@ -/* - * Copyright 2014 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.schema.pojos; - -import com.fasterxml.jackson.annotation.JsonIgnore; -import com.fasterxml.jackson.annotation.JsonInclude; -import com.fasterxml.jackson.annotation.JsonProperty; -import com.fasterxml.jackson.annotation.JsonRootName; -import com.fasterxml.jackson.databind.annotation.JsonDeserialize; -import com.fasterxml.jackson.databind.annotation.JsonSerialize; -import org.joda.time.DateTime; -import org.openmhealth.schema.pojos.generic.DescriptiveStatistic; -import org.openmhealth.schema.pojos.generic.TimeFrame; -import org.openmhealth.schema.pojos.serialize.LabeledEnumSerializer; -import org.openmhealth.schema.pojos.serialize.PositionDuringMeasurementDeserializer; - -import java.math.BigDecimal; - - -/** - * @author Danilo Bonilla - */ -@JsonInclude(JsonInclude.Include.NON_NULL) -@JsonRootName(value = BloodGlucose.SCHEMA_BLOOD_GLUCOSE, namespace = DataPoint.NAMESPACE) -public class BloodGlucose extends BaseDataPoint { - - @JsonProperty(value = "blood_glucose", required = true) - private BloodGlucoseUnitValue bloodGlucose; - - @JsonProperty(value = "effective_time_frame", required = false) - private TimeFrame effectiveTimeFrame; - - @JsonProperty(value = "descriptive_statistic", required = false) - private DescriptiveStatistic descriptiveStatistic; - - @JsonProperty(value = "user_notes", required = false) - private String notes; - - @JsonProperty(value = "temporal_relationship_to_meal", required = false) - private TemporalRelationshipToMeal temporalRelationshipToMeal; - - @JsonProperty(value = "temporal_relationship_to_sleep", required = false) - private TemporalRelationshipToSleep temporalRelationshipToSleep; - - @JsonProperty(value = "blood_specimen_type", required = false) - @JsonSerialize(using = LabeledEnumSerializer.class) - @JsonDeserialize(using = PositionDuringMeasurementDeserializer.class) - private BloodSpecimenType bloodSpecimenType; - - public static final String SCHEMA_BLOOD_GLUCOSE = "blood_glucose"; - - public BloodGlucose() { - } - - public BloodGlucose(BigDecimal value, BloodGlucoseUnitValue.Unit unit) { - this.bloodGlucose = new BloodGlucoseUnitValue(value, unit); - } - - @Override - @JsonIgnore - public String getSchemaName() { - return SCHEMA_BLOOD_GLUCOSE; - } - - @Override - @JsonIgnore - public DateTime getTimeStamp() { - return effectiveTimeFrame.getTimestamp(); - } - - public TimeFrame getEffectiveTimeFrame() { - return effectiveTimeFrame; - } - - public void setEffectiveTimeFrame(TimeFrame effectiveTimeFrame) { - this.effectiveTimeFrame = effectiveTimeFrame; - } - - public DescriptiveStatistic getDescriptiveStatistic() { - return descriptiveStatistic; - } - - public void setDescriptiveStatistic(DescriptiveStatistic descriptiveStatistic) { - this.descriptiveStatistic = descriptiveStatistic; - } - - public String getNotes() { - return notes; - } - - public void setNotes(String notes) { - this.notes = notes; - } - - public TemporalRelationshipToMeal getTemporalRelationshipToMeal() { - return temporalRelationshipToMeal; - } - - public void setTemporalRelationshipToMeal(TemporalRelationshipToMeal temporalRelationshipToMeal) { - this.temporalRelationshipToMeal = temporalRelationshipToMeal; - } - - public TemporalRelationshipToSleep getTemporalRelationshipToSleep() { - return temporalRelationshipToSleep; - } - - public void setTemporalRelationshipToSleep(TemporalRelationshipToSleep temporalRelationshipToSleep) { - this.temporalRelationshipToSleep = temporalRelationshipToSleep; - } - - public BloodSpecimenType getBloodSpecimenType() { - return bloodSpecimenType; - } - - public void setBloodSpecimenType(BloodSpecimenType bloodSpecimenType) { - this.bloodSpecimenType = bloodSpecimenType; - } - - public BloodGlucoseUnitValue getBloodGlucose() { - return bloodGlucose; - } - - public void setBloodGlucose(BloodGlucoseUnitValue bloodGlucose) { - this.bloodGlucose = bloodGlucose; - } -} diff --git a/java-schema-objects/src/main/java/org/openmhealth/schema/pojos/BloodGlucoseUnitValue.java b/java-schema-objects/src/main/java/org/openmhealth/schema/pojos/BloodGlucoseUnitValue.java deleted file mode 100644 index ae727083..00000000 --- a/java-schema-objects/src/main/java/org/openmhealth/schema/pojos/BloodGlucoseUnitValue.java +++ /dev/null @@ -1,94 +0,0 @@ -/* - * Copyright 2014 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.schema.pojos; - - -import com.fasterxml.jackson.annotation.JsonInclude; -import com.fasterxml.jackson.annotation.JsonProperty; -import com.fasterxml.jackson.databind.annotation.JsonDeserialize; -import com.fasterxml.jackson.databind.annotation.JsonSerialize; -import org.openmhealth.schema.pojos.serialize.BloodGlucoseUnitValueDeserializer; -import org.openmhealth.schema.pojos.serialize.LabeledEnumSerializer; - -import java.math.BigDecimal; - - -/** - * @author Danilo Bonilla - */ -@JsonInclude(JsonInclude.Include.NON_NULL) -public class BloodGlucoseUnitValue { - - @JsonProperty(value = "value", required = true) - private BigDecimal value; - - @JsonProperty(value = "unit", required = true) - @JsonSerialize(using = LabeledEnumSerializer.class) - @JsonDeserialize(using = BloodGlucoseUnitValueDeserializer.class) - private Unit unit; - - public enum Unit implements LabeledEnum { - - mg_dL("mg/dL"), - - mmol_L("mmol/L"); - - private String label; - - Unit(String label) { - this.label = label; - } - - @Override - public String getLabel() { - return label; - } - - public static Unit valueForLabel(String label) { - for (Unit val : Unit.values()) { - if (val.getLabel().equals(label)) { - return val; - } - } - return null; - } - } - - public BloodGlucoseUnitValue() { - } - - public BloodGlucoseUnitValue(BigDecimal value, Unit unit) { - this.value = value; - this.unit = unit; - } - - public BigDecimal getValue() { - return value; - } - - public void setValue(BigDecimal value) { - this.value = value; - } - - public Unit getUnit() { - return unit; - } - - public void setUnit(Unit unit) { - this.unit = unit; - } -} diff --git a/java-schema-objects/src/main/java/org/openmhealth/schema/pojos/BloodPressure.java b/java-schema-objects/src/main/java/org/openmhealth/schema/pojos/BloodPressure.java deleted file mode 100644 index 70bc53f1..00000000 --- a/java-schema-objects/src/main/java/org/openmhealth/schema/pojos/BloodPressure.java +++ /dev/null @@ -1,120 +0,0 @@ -/* - * Copyright 2014 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.schema.pojos; - -import com.fasterxml.jackson.annotation.JsonIgnore; -import com.fasterxml.jackson.annotation.JsonInclude; -import com.fasterxml.jackson.annotation.JsonProperty; -import com.fasterxml.jackson.annotation.JsonRootName; -import com.fasterxml.jackson.databind.annotation.JsonDeserialize; -import com.fasterxml.jackson.databind.annotation.JsonSerialize; -import org.joda.time.DateTime; -import org.openmhealth.schema.pojos.generic.DescriptiveStatistic; -import org.openmhealth.schema.pojos.generic.TimeFrame; -import org.openmhealth.schema.pojos.serialize.LabeledEnumSerializer; -import org.openmhealth.schema.pojos.serialize.PositionDuringMeasurementDeserializer; - - -/** - * @author Danilo Bonilla - */ -@JsonInclude(JsonInclude.Include.NON_NULL) -@JsonRootName(value = BloodPressure.SCHEMA_BLOOD_PRESSURE, namespace = DataPoint.NAMESPACE) -public class BloodPressure extends BaseDataPoint { - - @JsonProperty(value = "systolic_blood_pressure", required = false) - private SystolicBloodPressure systolic; - - @JsonProperty(value = "diastolic_blood_pressure", required = false) - private DiastolicBloodPressure diastolic; - - @JsonProperty(value = "effective_time_frame", required = false) - private TimeFrame effectiveTimeFrame; - - @JsonProperty(value = "position_during_measurement", required = false) - @JsonSerialize(using = LabeledEnumSerializer.class) - @JsonDeserialize(using = PositionDuringMeasurementDeserializer.class) - private PositionDuringMeasurement positionDuringMeasurement; - - @JsonProperty(value = "descriptive_statistic", required = false) - private DescriptiveStatistic descriptiveStatistic; - - public static final String SCHEMA_BLOOD_PRESSURE = "blood_pressure"; - - @JsonProperty(value = "user_notes", required = false) - private String notes; - - @Override - @JsonIgnore - public String getSchemaName() { - return SCHEMA_BLOOD_PRESSURE; - } - - @Override - @JsonIgnore - public DateTime getTimeStamp() { - return effectiveTimeFrame.getTimestamp(); - } - - public TimeFrame getEffectiveTimeFrame() { - return effectiveTimeFrame; - } - - public void setEffectiveTimeFrame(TimeFrame effectiveTimeFrame) { - this.effectiveTimeFrame = effectiveTimeFrame; - } - - public PositionDuringMeasurement getPositionDuringMeasurement() { - return positionDuringMeasurement; - } - - public void setPositionDuringMeasurement(PositionDuringMeasurement positionDuringMeasurement) { - this.positionDuringMeasurement = positionDuringMeasurement; - } - - public SystolicBloodPressure getSystolic() { - return systolic; - } - - public void setSystolic(SystolicBloodPressure systolic) { - this.systolic = systolic; - } - - public DiastolicBloodPressure getDiastolic() { - return diastolic; - } - - public void setDiastolic(DiastolicBloodPressure diastolic) { - this.diastolic = diastolic; - } - - public String getNotes() { - return notes; - } - - public void setNotes(String notes) { - this.notes = notes; - } - - public DescriptiveStatistic getDescriptiveStatistic() { - return descriptiveStatistic; - } - - public void setDescriptiveStatistic(DescriptiveStatistic descriptiveStatistic) { - this.descriptiveStatistic = descriptiveStatistic; - } -} diff --git a/java-schema-objects/src/main/java/org/openmhealth/schema/pojos/BloodPressureUnit.java b/java-schema-objects/src/main/java/org/openmhealth/schema/pojos/BloodPressureUnit.java deleted file mode 100644 index 82bf2db5..00000000 --- a/java-schema-objects/src/main/java/org/openmhealth/schema/pojos/BloodPressureUnit.java +++ /dev/null @@ -1,24 +0,0 @@ -/* - * Copyright 2014 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.schema.pojos; - -/** - * @author Danilo Bonilla - */ -public enum BloodPressureUnit { - mmHg -} diff --git a/java-schema-objects/src/main/java/org/openmhealth/schema/pojos/BloodSpecimenType.java b/java-schema-objects/src/main/java/org/openmhealth/schema/pojos/BloodSpecimenType.java deleted file mode 100644 index e0294c6a..00000000 --- a/java-schema-objects/src/main/java/org/openmhealth/schema/pojos/BloodSpecimenType.java +++ /dev/null @@ -1,46 +0,0 @@ -/* - * Copyright 2014 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.schema.pojos; - -/** - * @author Danilo Bonilla - */ -public enum BloodSpecimenType implements LabeledEnum { - - whole_blood("whole blood"), plasma("plasma"); - - private String label; - - BloodSpecimenType(String label) { - this.label = label; - } - - @Override - public String getLabel() { - return label; - } - - public static BloodSpecimenType valueForLabel(String label) { - for (BloodSpecimenType value : BloodSpecimenType.values()) { - if (value.getLabel().equals(label)) { - return value; - } - } - return null; - } - -} diff --git a/java-schema-objects/src/main/java/org/openmhealth/schema/pojos/BodyHeight.java b/java-schema-objects/src/main/java/org/openmhealth/schema/pojos/BodyHeight.java deleted file mode 100644 index 0c486f0c..00000000 --- a/java-schema-objects/src/main/java/org/openmhealth/schema/pojos/BodyHeight.java +++ /dev/null @@ -1,90 +0,0 @@ -/* - * Copyright 2014 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.schema.pojos; - -import com.fasterxml.jackson.annotation.JsonIgnore; -import com.fasterxml.jackson.annotation.JsonInclude; -import com.fasterxml.jackson.annotation.JsonProperty; -import com.fasterxml.jackson.annotation.JsonRootName; -import org.joda.time.DateTime; -import org.openmhealth.schema.pojos.generic.DescriptiveStatistic; -import org.openmhealth.schema.pojos.generic.LengthUnitValue; -import org.openmhealth.schema.pojos.generic.TimeFrame; - - -/** - * @author Danilo Bonilla - */ -@JsonInclude(JsonInclude.Include.NON_NULL) -@JsonRootName(value = BodyHeight.SCHEMA_BODY_HEIGHT, namespace = DataPoint.NAMESPACE) -public class BodyHeight extends BaseDataPoint { - - @JsonProperty(value = "body_height", required = true) - private LengthUnitValue lengthUnitValue; - - @JsonProperty(value = "effective_time_frame", required = false) - private TimeFrame effectiveTimeFrame; - - @JsonProperty(value = "descriptive_statistic", required = false) - private DescriptiveStatistic descriptiveStatistic; - - public static final String SCHEMA_BODY_HEIGHT = "body_height"; - - public BodyHeight() { - } - - public BodyHeight(LengthUnitValue lengthUnitValue, TimeFrame effectiveTimeFrame) { - this.lengthUnitValue = lengthUnitValue; - this.effectiveTimeFrame = effectiveTimeFrame; - } - - @Override - @JsonIgnore - public String getSchemaName() { - return SCHEMA_BODY_HEIGHT; - } - - @Override - @JsonIgnore - public DateTime getTimeStamp() { - return effectiveTimeFrame.getTimestamp(); - } - - public DescriptiveStatistic getDescriptiveStatistic() { - return descriptiveStatistic; - } - - public void setDescriptiveStatistic(DescriptiveStatistic descriptiveStatistic) { - this.descriptiveStatistic = descriptiveStatistic; - } - - public LengthUnitValue getLengthUnitValue() { - return lengthUnitValue; - } - - public void setLengthUnitValue(LengthUnitValue lengthUnitValue) { - this.lengthUnitValue = lengthUnitValue; - } - - public TimeFrame getEffectiveTimeFrame() { - return effectiveTimeFrame; - } - - public void setEffectiveTimeFrame(TimeFrame effectiveTimeFrame) { - this.effectiveTimeFrame = effectiveTimeFrame; - } -} diff --git a/java-schema-objects/src/main/java/org/openmhealth/schema/pojos/BodyWeight.java b/java-schema-objects/src/main/java/org/openmhealth/schema/pojos/BodyWeight.java deleted file mode 100644 index 9e84a6ae..00000000 --- a/java-schema-objects/src/main/java/org/openmhealth/schema/pojos/BodyWeight.java +++ /dev/null @@ -1,85 +0,0 @@ -/* - * Copyright 2014 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.schema.pojos; - -import com.fasterxml.jackson.annotation.JsonIgnore; -import com.fasterxml.jackson.annotation.JsonInclude; -import com.fasterxml.jackson.annotation.JsonProperty; -import com.fasterxml.jackson.annotation.JsonRootName; -import org.joda.time.DateTime; -import org.openmhealth.schema.pojos.generic.DescriptiveStatistic; -import org.openmhealth.schema.pojos.generic.MassUnitValue; -import org.openmhealth.schema.pojos.generic.TimeFrame; - - -/** - * @author Danilo Bonilla - */ -@JsonInclude(JsonInclude.Include.NON_NULL) -@JsonRootName(value = BodyWeight.SCHEMA_BODY_WEIGHT, namespace = "omh:normalized") -public class BodyWeight extends BaseDataPoint { - - @JsonProperty(value = "body_weight", required = true) - private MassUnitValue massUnitValue; - - @JsonProperty(value = "effective_time_frame", required = false) - private TimeFrame effectiveTimeFrame; - - @JsonProperty(value = "descriptive_statistic", required = false) - private DescriptiveStatistic descriptiveStatistic; - - public static final String SCHEMA_BODY_WEIGHT = "body_weight"; - - public BodyWeight() { - } - - @Override - @JsonIgnore - public String getSchemaName() { - return SCHEMA_BODY_WEIGHT; - } - - @Override - @JsonIgnore - public DateTime getTimeStamp() { - return effectiveTimeFrame.getTimestamp(); - } - - public DescriptiveStatistic getDescriptiveStatistic() { - return descriptiveStatistic; - } - - public void setDescriptiveStatistic(DescriptiveStatistic descriptiveStatistic) { - this.descriptiveStatistic = descriptiveStatistic; - } - - public MassUnitValue getMassUnitValue() { - return massUnitValue; - } - - public void setMassUnitValue(MassUnitValue massUnitValue) { - this.massUnitValue = massUnitValue; - } - - public TimeFrame getEffectiveTimeFrame() { - return effectiveTimeFrame; - } - - public void setEffectiveTimeFrame(TimeFrame effectiveTimeFrame) { - this.effectiveTimeFrame = effectiveTimeFrame; - } -} diff --git a/java-schema-objects/src/main/java/org/openmhealth/schema/pojos/DataPoint.java b/java-schema-objects/src/main/java/org/openmhealth/schema/pojos/DataPoint.java deleted file mode 100644 index b1dc559e..00000000 --- a/java-schema-objects/src/main/java/org/openmhealth/schema/pojos/DataPoint.java +++ /dev/null @@ -1,35 +0,0 @@ -/* - * Copyright 2014 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.schema.pojos; - -import org.joda.time.DateTime; - - -/** - * @author Danilo Bonilla - */ -public interface DataPoint { - - public static final String NAMESPACE = "omh:normalized"; - - String getSchemaName(); - - DateTime getTimeStamp(); - - Metadata getMetadata(); - -} diff --git a/java-schema-objects/src/main/java/org/openmhealth/schema/pojos/DiastolicBloodPressure.java b/java-schema-objects/src/main/java/org/openmhealth/schema/pojos/DiastolicBloodPressure.java deleted file mode 100644 index 2c23dd93..00000000 --- a/java-schema-objects/src/main/java/org/openmhealth/schema/pojos/DiastolicBloodPressure.java +++ /dev/null @@ -1,60 +0,0 @@ -/* - * Copyright 2014 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.schema.pojos; - -import com.fasterxml.jackson.annotation.JsonInclude; -import com.fasterxml.jackson.annotation.JsonProperty; - -import java.math.BigDecimal; - - -/** - * @author Danilo Bonilla - */ -@JsonInclude(JsonInclude.Include.NON_NULL) -public class DiastolicBloodPressure { - - @JsonProperty(value = "value", required = true) - private BigDecimal value; - - @JsonProperty(value = "unit", required = true) - private BloodPressureUnit unit; - - public DiastolicBloodPressure() { - } - - public DiastolicBloodPressure(BigDecimal value, BloodPressureUnit unit) { - this.value = value; - this.unit = unit; - } - - public BigDecimal getValue() { - return value; - } - - public void setValue(BigDecimal value) { - this.value = value; - } - - public BloodPressureUnit getUnit() { - return unit; - } - - public void setUnit(BloodPressureUnit unit) { - this.unit = unit; - } -} diff --git a/java-schema-objects/src/main/java/org/openmhealth/schema/pojos/HeartRate.java b/java-schema-objects/src/main/java/org/openmhealth/schema/pojos/HeartRate.java deleted file mode 100644 index c2541406..00000000 --- a/java-schema-objects/src/main/java/org/openmhealth/schema/pojos/HeartRate.java +++ /dev/null @@ -1,119 +0,0 @@ -/* - * Copyright 2014 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.schema.pojos; - -import com.fasterxml.jackson.annotation.JsonIgnore; -import com.fasterxml.jackson.annotation.JsonInclude; -import com.fasterxml.jackson.annotation.JsonProperty; -import com.fasterxml.jackson.annotation.JsonRootName; -import com.fasterxml.jackson.databind.annotation.JsonDeserialize; -import com.fasterxml.jackson.databind.annotation.JsonSerialize; -import org.joda.time.DateTime; -import org.openmhealth.schema.pojos.generic.DescriptiveStatistic; -import org.openmhealth.schema.pojos.generic.TimeFrame; -import org.openmhealth.schema.pojos.serialize.LabeledEnumSerializer; -import org.openmhealth.schema.pojos.serialize.TemporalRelationshipToPhysicalActivityDeserializer; - - -/** - * @author Danilo Bonilla - */ -@JsonInclude(JsonInclude.Include.NON_NULL) -@JsonRootName(value = HeartRate.SCHEMA_HEART_RATE, namespace = DataPoint.NAMESPACE) -public class HeartRate extends BaseDataPoint { - - @JsonProperty(value = "effective_time_frame", required = false) - private TimeFrame effectiveTimeFrame; - - @JsonProperty(value = "descriptive_statistic", required = false) - private DescriptiveStatistic descriptiveStatistic; - - @JsonProperty(value = "user_notes", required = false) - private String userNotes; - - @JsonProperty(value = "heart_rate", required = true) - private HeartRateUnitValue heartRate; - - @JsonProperty(value = "temporal_relationship_to_physical_activity", required = false) - @JsonSerialize(using = LabeledEnumSerializer.class) - @JsonDeserialize(using = TemporalRelationshipToPhysicalActivityDeserializer.class) - private TemporalRelationshipToPhysicalActivity temporalRelationshipToPhysicalActivity; - - public static final String SCHEMA_HEART_RATE = "heart_rate"; - - public HeartRate() { - } - - public HeartRate(Integer value, HeartRateUnitValue.Unit unit) { - this.heartRate = new HeartRateUnitValue(value, unit); - } - - @Override - @JsonIgnore - public String getSchemaName() { - return SCHEMA_HEART_RATE; - } - - @Override - @JsonIgnore - public DateTime getTimeStamp() { - return effectiveTimeFrame.getTimestamp(); - } - - public DescriptiveStatistic getDescriptiveStatistic() { - return descriptiveStatistic; - } - - public void setDescriptiveStatistic(DescriptiveStatistic descriptiveStatistic) { - this.descriptiveStatistic = descriptiveStatistic; - } - - public TimeFrame getEffectiveTimeFrame() { - return effectiveTimeFrame; - } - - public void setEffectiveTimeFrame(TimeFrame effectiveTimeFrame) { - this.effectiveTimeFrame = effectiveTimeFrame; - } - - public String getUserNotes() { - return userNotes; - } - - public void setUserNotes(String userNotes) { - this.userNotes = userNotes; - } - - public HeartRateUnitValue getHeartRate() { - return heartRate; - } - - public void setHeartRate(HeartRateUnitValue heartRate) { - this.heartRate = heartRate; - } - - public TemporalRelationshipToPhysicalActivity getTemporalRelationshipToPhysicalActivity() { - return temporalRelationshipToPhysicalActivity; - } - - public void setTemporalRelationshipToPhysicalActivity( - TemporalRelationshipToPhysicalActivity temporalRelationshipToPhysicalActivity) { - this.temporalRelationshipToPhysicalActivity = temporalRelationshipToPhysicalActivity; - } - - -} diff --git a/java-schema-objects/src/main/java/org/openmhealth/schema/pojos/HeartRateUnitValue.java b/java-schema-objects/src/main/java/org/openmhealth/schema/pojos/HeartRateUnitValue.java deleted file mode 100644 index a801892d..00000000 --- a/java-schema-objects/src/main/java/org/openmhealth/schema/pojos/HeartRateUnitValue.java +++ /dev/null @@ -1,90 +0,0 @@ - -/* - * Copyright 2014 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.schema.pojos; - -import com.fasterxml.jackson.annotation.JsonInclude; -import com.fasterxml.jackson.annotation.JsonProperty; -import com.fasterxml.jackson.databind.annotation.JsonDeserialize; -import com.fasterxml.jackson.databind.annotation.JsonSerialize; -import org.openmhealth.schema.pojos.serialize.HeartRateUnitDeserializer; -import org.openmhealth.schema.pojos.serialize.LabeledEnumSerializer; - - -/** - * @author Danilo Bonilla - */ -@JsonInclude(JsonInclude.Include.NON_NULL) -public class HeartRateUnitValue { - - @JsonProperty(value = "value", required = true) - private Integer value; - - @JsonProperty(value = "unit", required = true) - @JsonSerialize(using = LabeledEnumSerializer.class) - @JsonDeserialize(using = HeartRateUnitDeserializer.class) - private Unit unit; - - public enum Unit implements LabeledEnum { - - bpm("beats/min"); - - private String label; - - Unit(String label) { - this.label = label; - } - - @Override - public String getLabel() { - return label; - } - - public static Unit valueForLabel(String label) { - for (Unit unit : Unit.values()) { - if (unit.getLabel().equals(label)) { - return unit; - } - } - return null; - } - } - - public HeartRateUnitValue() { - } - - public HeartRateUnitValue(Integer value, Unit unit) { - this.value = value; - this.unit = unit; - } - - public Integer getValue() { - return value; - } - - public void setValue(Integer value) { - this.value = value; - } - - public Unit getUnit() { - return unit; - } - - public void setUnit(Unit unit) { - this.unit = unit; - } -} diff --git a/java-schema-objects/src/main/java/org/openmhealth/schema/pojos/Metadata.java b/java-schema-objects/src/main/java/org/openmhealth/schema/pojos/Metadata.java deleted file mode 100644 index f7fbb973..00000000 --- a/java-schema-objects/src/main/java/org/openmhealth/schema/pojos/Metadata.java +++ /dev/null @@ -1,62 +0,0 @@ -/* - * Copyright 2014 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.schema.pojos; - -import com.fasterxml.jackson.annotation.JsonProperty; -import com.fasterxml.jackson.annotation.JsonRootName; -import com.fasterxml.jackson.databind.annotation.JsonDeserialize; -import com.fasterxml.jackson.databind.annotation.JsonSerialize; -import org.joda.time.DateTime; -import org.openmhealth.schema.pojos.serialize.dates.MetadataTimestampDeserializer; -import org.openmhealth.schema.pojos.serialize.dates.MetadataTimestampSerializer; - - -/** - * @author Danilo Bonilla - */ -@JsonRootName(value = "metadata", namespace = "org.openmhealth.schema") -public class Metadata { - - @JsonProperty(value = "timestamp") - @JsonSerialize(using = MetadataTimestampSerializer.class) - @JsonDeserialize(using = MetadataTimestampDeserializer.class) - private DateTime timestamp; - - @JsonProperty(value = "shim") - private String shimKey; - - public final static String TIMESTAMP_FORMAT = "yyyy-MM-dd'T'HH:mm:ss.SSSZ"; - - public Metadata() { - } - - public DateTime getTimestamp() { - return timestamp; - } - - public void setTimestamp(DateTime timestamp) { - this.timestamp = timestamp; - } - - public String getShimKey() { - return shimKey; - } - - public void setShimKey(String shimKey) { - this.shimKey = shimKey; - } -} \ No newline at end of file diff --git a/java-schema-objects/src/main/java/org/openmhealth/schema/pojos/PositionDuringMeasurement.java b/java-schema-objects/src/main/java/org/openmhealth/schema/pojos/PositionDuringMeasurement.java deleted file mode 100644 index 25f39900..00000000 --- a/java-schema-objects/src/main/java/org/openmhealth/schema/pojos/PositionDuringMeasurement.java +++ /dev/null @@ -1,49 +0,0 @@ -/* - * Copyright 2014 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.schema.pojos; - -/** - * Describes the position the patient was in - * when a clinical measurement was taken. - * - * @author Danilo Bonilla - */ -public enum PositionDuringMeasurement implements LabeledEnum { - - sitting("sitting"), lying_down("lying down"), standing("standing"); - - private String label; - - PositionDuringMeasurement(String label) { - this.label = label; - } - - @Override - public String getLabel() { - return label; - } - - public static PositionDuringMeasurement valueForLabel(String label) { - for (PositionDuringMeasurement value : PositionDuringMeasurement.values()) { - if (value.getLabel().equals(label)) { - return value; - } - } - return null; - } - -} diff --git a/java-schema-objects/src/main/java/org/openmhealth/schema/pojos/SchemaPojoUtils.java b/java-schema-objects/src/main/java/org/openmhealth/schema/pojos/SchemaPojoUtils.java deleted file mode 100644 index 74b1e082..00000000 --- a/java-schema-objects/src/main/java/org/openmhealth/schema/pojos/SchemaPojoUtils.java +++ /dev/null @@ -1,58 +0,0 @@ -/* - * Copyright 2014 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.schema.pojos; - -import java.util.HashMap; -import java.util.Map; - - -/** - * @author Danilo Bonilla - */ -public class SchemaPojoUtils { - - //todo: find better way with reflection? - private final static Map> schemaToNormalizedTypeMap = - new HashMap>() {{ - put(Activity.SCHEMA_ACTIVITY, Activity.class); - put(StepCount.SCHEMA_STEP_COUNT, StepCount.class); - put(BodyWeight.SCHEMA_BODY_WEIGHT, BodyWeight.class); - put(BodyHeight.SCHEMA_BODY_HEIGHT, BodyHeight.class); - put(BloodPressure.SCHEMA_BLOOD_PRESSURE, BloodPressure.class); - put(BloodGlucose.SCHEMA_BLOOD_GLUCOSE, BloodGlucose.class); - put(SleepDuration.SCHEMA_SLEEP_DURATION, SleepDuration.class); - put(HeartRate.SCHEMA_HEART_RATE, HeartRate.class); - }}; - - public static Class getSchemaClass(String schemaName) { - String clean = schemaName.trim().toLowerCase(); - if (schemaToNormalizedTypeMap.containsKey(clean)) { - return schemaToNormalizedTypeMap.get(clean); - } - return null; - } - - public static String getSchemaName(Class givenClazz) { - for (String schemaName : schemaToNormalizedTypeMap.keySet()) { - Class clazz = schemaToNormalizedTypeMap.get(schemaName); - if (clazz.equals(givenClazz)) { - return schemaName; - } - } - return null; - } -} diff --git a/java-schema-objects/src/main/java/org/openmhealth/schema/pojos/SleepDuration.java b/java-schema-objects/src/main/java/org/openmhealth/schema/pojos/SleepDuration.java deleted file mode 100644 index 957de381..00000000 --- a/java-schema-objects/src/main/java/org/openmhealth/schema/pojos/SleepDuration.java +++ /dev/null @@ -1,83 +0,0 @@ -/* - * Copyright 2014 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.schema.pojos; - -import com.fasterxml.jackson.annotation.JsonIgnore; -import com.fasterxml.jackson.annotation.JsonInclude; -import com.fasterxml.jackson.annotation.JsonProperty; -import com.fasterxml.jackson.annotation.JsonRootName; -import org.joda.time.DateTime; -import org.openmhealth.schema.pojos.generic.TimeFrame; - - -/** - * @author Danilo Bonilla - */ -@JsonInclude(JsonInclude.Include.NON_NULL) -@JsonRootName(value = SleepDuration.SCHEMA_SLEEP_DURATION, namespace = DataPoint.NAMESPACE) -public class SleepDuration extends BaseDataPoint { - - @JsonProperty(value = "effective_time_frame") - private TimeFrame effectiveTimeFrame; - - @JsonProperty(value = "sleep_duration") - private SleepDurationUnitValue sleepDurationUnitValue; - - @JsonProperty(value = "user_notes", required = false) - private String notes; - - public static final String SCHEMA_SLEEP_DURATION = "sleep_duration"; - - public SleepDuration() { - } - - @Override - @JsonIgnore - public String getSchemaName() { - return SCHEMA_SLEEP_DURATION; - } - - @Override - @JsonIgnore - public DateTime getTimeStamp() { - return effectiveTimeFrame.getTimestamp(); - } - - public TimeFrame getEffectiveTimeFrame() { - return effectiveTimeFrame; - } - - public void setEffectiveTimeFrame(TimeFrame effectiveTimeFrame) { - this.effectiveTimeFrame = effectiveTimeFrame; - } - - public SleepDurationUnitValue getSleepDurationUnitValue() { - return sleepDurationUnitValue; - } - - public void setSleepDurationUnitValue(SleepDurationUnitValue sleepDurationUnitValue) { - this.sleepDurationUnitValue = sleepDurationUnitValue; - } - - public String getNotes() { - return notes; - } - - public void setNotes(String notes) { - this.notes = notes; - } -} diff --git a/java-schema-objects/src/main/java/org/openmhealth/schema/pojos/SleepDurationUnitValue.java b/java-schema-objects/src/main/java/org/openmhealth/schema/pojos/SleepDurationUnitValue.java deleted file mode 100644 index 5f978db5..00000000 --- a/java-schema-objects/src/main/java/org/openmhealth/schema/pojos/SleepDurationUnitValue.java +++ /dev/null @@ -1,62 +0,0 @@ -/* - * Copyright 2014 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.schema.pojos; - -import com.fasterxml.jackson.annotation.JsonInclude; -import com.fasterxml.jackson.annotation.JsonProperty; - -import java.math.BigDecimal; - - -/** - * @author Danilo Bonilla - */ -@JsonInclude(JsonInclude.Include.NON_NULL) -public class SleepDurationUnitValue { - - @JsonProperty(value = "value", required = true) - private BigDecimal value; - - @JsonProperty(value = "unit", required = true) - private Unit unit; - - public enum Unit {min, h} - - public SleepDurationUnitValue() { - } - - public SleepDurationUnitValue(BigDecimal value, Unit unit) { - this.value = value; - this.unit = unit; - } - - public BigDecimal getValue() { - return value; - } - - public void setValue(BigDecimal value) { - this.value = value; - } - - public Unit getUnit() { - return unit; - } - - public void setUnit(Unit unit) { - this.unit = unit; - } -} diff --git a/java-schema-objects/src/main/java/org/openmhealth/schema/pojos/StepCount.java b/java-schema-objects/src/main/java/org/openmhealth/schema/pojos/StepCount.java deleted file mode 100644 index 66003cfc..00000000 --- a/java-schema-objects/src/main/java/org/openmhealth/schema/pojos/StepCount.java +++ /dev/null @@ -1,72 +0,0 @@ -/* - * Copyright 2014 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.schema.pojos; - -import com.fasterxml.jackson.annotation.JsonIgnore; -import com.fasterxml.jackson.annotation.JsonInclude; -import com.fasterxml.jackson.annotation.JsonProperty; -import com.fasterxml.jackson.annotation.JsonRootName; -import org.joda.time.DateTime; -import org.openmhealth.schema.pojos.generic.TimeFrame; - - -/** - * @author Danilo Bonilla - */ -@JsonInclude(JsonInclude.Include.NON_NULL) -@JsonRootName(value = StepCount.SCHEMA_STEP_COUNT, namespace = DataPoint.NAMESPACE) -public class StepCount extends BaseDataPoint { - - @JsonProperty(value = "step_count", required = true) - private Integer stepCount; - - @JsonProperty(value = "effective_time_frame", required = false) - private TimeFrame effectiveTimeFrame; - - public static final String SCHEMA_STEP_COUNT = "step_count"; - - public StepCount() { - } - - @Override - @JsonIgnore - public String getSchemaName() { - return SCHEMA_STEP_COUNT; - } - - @Override - @JsonIgnore - public DateTime getTimeStamp() { - return effectiveTimeFrame.getTimestamp(); - } - - public Integer getStepCount() { - return stepCount; - } - - public void setStepCount(Integer stepCount) { - this.stepCount = stepCount; - } - - public TimeFrame getEffectiveTimeFrame() { - return effectiveTimeFrame; - } - - public void setEffectiveTimeFrame(TimeFrame effectiveTimeFrame) { - this.effectiveTimeFrame = effectiveTimeFrame; - } -} diff --git a/java-schema-objects/src/main/java/org/openmhealth/schema/pojos/SystolicBloodPressure.java b/java-schema-objects/src/main/java/org/openmhealth/schema/pojos/SystolicBloodPressure.java deleted file mode 100644 index 984b0462..00000000 --- a/java-schema-objects/src/main/java/org/openmhealth/schema/pojos/SystolicBloodPressure.java +++ /dev/null @@ -1,62 +0,0 @@ -/* - * Copyright 2014 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.schema.pojos; - -import com.fasterxml.jackson.annotation.JsonInclude; -import com.fasterxml.jackson.annotation.JsonProperty; - -import java.math.BigDecimal; - - -/** - * @author Danilo Bonilla - */ -@JsonInclude(JsonInclude.Include.NON_NULL) -public class SystolicBloodPressure { - - @JsonProperty(value = "value", required = true) - private BigDecimal value; - - @JsonProperty(value = "unit", required = true) - private BloodPressureUnit unit; - - public SystolicBloodPressure() { - } - - public SystolicBloodPressure(BigDecimal value, BloodPressureUnit unit) { - this.value = value; - this.unit = unit; - } - - public enum Unit {mmHg} - - public BigDecimal getValue() { - return value; - } - - public void setValue(BigDecimal value) { - this.value = value; - } - - public BloodPressureUnit getUnit() { - return unit; - } - - public void setUnit(BloodPressureUnit unit) { - this.unit = unit; - } -} diff --git a/java-schema-objects/src/main/java/org/openmhealth/schema/pojos/TemporalRelationshipToMeal.java b/java-schema-objects/src/main/java/org/openmhealth/schema/pojos/TemporalRelationshipToMeal.java deleted file mode 100644 index 8ee4ccb9..00000000 --- a/java-schema-objects/src/main/java/org/openmhealth/schema/pojos/TemporalRelationshipToMeal.java +++ /dev/null @@ -1,65 +0,0 @@ -/* - * Copyright 2014 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.schema.pojos; - -/** - * @author Danilo Bonilla - */ -public enum TemporalRelationshipToMeal implements LabeledEnum { - - fasting("fasting"), - - not_fasting("not fasting"), - - before_meal("before meal"), - - after_meal("after meal"), - - before_breakfast("before_breakfast"), - - after_breakfast("after breakfast"), - - before_lunch("before lunch"), - - after_lunch("after lunch"), - - before_dinner("before dinner"), - - after_dinner("after dinner"), - - two_hours_postprandial("2 hours postprandial"); - - private String label; - - TemporalRelationshipToMeal(String label) { - this.label = label; - } - - @Override - public String getLabel() { - return label; - } - - public static TemporalRelationshipToMeal valueForLabel(String label) { - for (TemporalRelationshipToMeal val : TemporalRelationshipToMeal.values()) { - if (val.getLabel().equals(label)) { - return val; - } - } - return null; - } -} diff --git a/java-schema-objects/src/main/java/org/openmhealth/schema/pojos/TemporalRelationshipToPhysicalActivity.java b/java-schema-objects/src/main/java/org/openmhealth/schema/pojos/TemporalRelationshipToPhysicalActivity.java deleted file mode 100644 index 5aaac60c..00000000 --- a/java-schema-objects/src/main/java/org/openmhealth/schema/pojos/TemporalRelationshipToPhysicalActivity.java +++ /dev/null @@ -1,53 +0,0 @@ -/* - * Copyright 2014 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.schema.pojos; - -/** - * @author Danilo Bonilla - */ -public enum TemporalRelationshipToPhysicalActivity implements LabeledEnum { - - at_rest("at rest"), - active("active"), - before_exercise("before exercise"), - after_exercise("after exercise"), - during_exercise("during exercise"); - - private String label; - - TemporalRelationshipToPhysicalActivity(String label) { - this.label = label; - } - - @Override - public String getLabel() { - return label; - } - - public static TemporalRelationshipToPhysicalActivity valueForLabel(String label) { - for ( - TemporalRelationshipToPhysicalActivity val : - TemporalRelationshipToPhysicalActivity.values() - ) { - if (val.getLabel().equals(label)) { - return val; - } - } - return null; - } - -} diff --git a/java-schema-objects/src/main/java/org/openmhealth/schema/pojos/TemporalRelationshipToSleep.java b/java-schema-objects/src/main/java/org/openmhealth/schema/pojos/TemporalRelationshipToSleep.java deleted file mode 100644 index db4328ba..00000000 --- a/java-schema-objects/src/main/java/org/openmhealth/schema/pojos/TemporalRelationshipToSleep.java +++ /dev/null @@ -1,50 +0,0 @@ -/* - * Copyright 2014 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.schema.pojos; - -/** - * @author Danilo Bonilla - */ -public enum TemporalRelationshipToSleep implements LabeledEnum{ - - before_sleeping("before sleeping"), - - during_sleep("during sleep"), - - on_waking("on waking"); - - private String label; - - TemporalRelationshipToSleep(String label) { - this.label = label; - } - - @Override - public String getLabel() { - return label; - } - - public static TemporalRelationshipToSleep valueForLabel(String label) { - for (TemporalRelationshipToSleep val : TemporalRelationshipToSleep.values()) { - if (val.getLabel().equals(label)) { - return val; - } - } - return null; - } - -} diff --git a/java-schema-objects/src/main/java/org/openmhealth/schema/pojos/build/ActivityBuilder.java b/java-schema-objects/src/main/java/org/openmhealth/schema/pojos/build/ActivityBuilder.java deleted file mode 100644 index eec59a66..00000000 --- a/java-schema-objects/src/main/java/org/openmhealth/schema/pojos/build/ActivityBuilder.java +++ /dev/null @@ -1,73 +0,0 @@ -/* - * Copyright 2014 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.schema.pojos.build; - -import org.joda.time.DateTime; -import org.openmhealth.schema.pojos.Activity; -import org.openmhealth.schema.pojos.generic.DurationUnitValue; -import org.openmhealth.schema.pojos.generic.LengthUnitValue; -import org.openmhealth.schema.pojos.generic.TimeFrame; - -import java.math.BigDecimal; - - -/** - * @author Danilo Bonilla - */ -public class ActivityBuilder implements SchemaPojoBuilder { - - private Activity activity; - - @Override - public Activity build() { - return activity; - } - - public ActivityBuilder() { - activity = new Activity(); - activity.setEffectiveTimeFrame(new TimeFrame()); - } - - public ActivityBuilder setActivityName(String activityName) { - activity.setActivityName(activityName); - return this; - } - - public ActivityBuilder setDistance(Double value, LengthUnitValue.LengthUnit unit) { - LengthUnitValue distance = new LengthUnitValue(); - distance.setValue(new BigDecimal(value)); - distance.setUnit(unit); - activity.setDistance(distance); - return this; - } - - public ActivityBuilder setReportedActivityIntensity(Activity.ActivityIntensity activityIntensity) { - activity.setReportedActivityIntensity(activityIntensity); - return this; - } - - public ActivityBuilder withStartAndDuration(DateTime start, Double value, - DurationUnitValue.DurationUnit unit) { - activity.setEffectiveTimeFrame(TimeFrame.withTimeInterval(start, value, unit)); - return this; - } - - public ActivityBuilder withStartAndEnd(DateTime start, DateTime end) { - activity.setEffectiveTimeFrame(TimeFrame.withTimeInterval(start, end)); - return this; - } -} diff --git a/java-schema-objects/src/main/java/org/openmhealth/schema/pojos/build/BloodGlucoseBuilder.java b/java-schema-objects/src/main/java/org/openmhealth/schema/pojos/build/BloodGlucoseBuilder.java deleted file mode 100644 index ab8f145f..00000000 --- a/java-schema-objects/src/main/java/org/openmhealth/schema/pojos/build/BloodGlucoseBuilder.java +++ /dev/null @@ -1,88 +0,0 @@ -/* - * Copyright 2014 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.schema.pojos.build; - -import org.joda.time.DateTime; -import org.openmhealth.schema.pojos.BloodGlucose; -import org.openmhealth.schema.pojos.BloodGlucoseUnitValue; -import org.openmhealth.schema.pojos.BloodSpecimenType; -import org.openmhealth.schema.pojos.TemporalRelationshipToMeal; -import org.openmhealth.schema.pojos.generic.DescriptiveStatistic; -import org.openmhealth.schema.pojos.generic.TimeFrame; - -import java.math.BigDecimal; - - -/** - * @author Danilo Bonilla - */ -public class BloodGlucoseBuilder implements SchemaPojoBuilder { - - private BloodGlucose bloodGlucose; - - public BloodGlucoseBuilder() { - bloodGlucose = new BloodGlucose(); - bloodGlucose.setEffectiveTimeFrame(new TimeFrame()); - } - - public BloodGlucoseBuilder setMgdLValue(BigDecimal value) { - bloodGlucose.setBloodGlucose( - new BloodGlucoseUnitValue(value, BloodGlucoseUnitValue.Unit.mg_dL)); - return this; - } - - public BloodGlucoseBuilder setValueAndUnit( - BigDecimal value, BloodGlucoseUnitValue.Unit unit) { - bloodGlucose.setBloodGlucose(new BloodGlucoseUnitValue(value, unit)); - return this; - } - - public BloodGlucoseBuilder setTemporalRelationshipToMeal( - TemporalRelationshipToMeal mealContext) { - if (mealContext != null) { - bloodGlucose.setTemporalRelationshipToMeal(mealContext); - } - return this; - } - - public BloodGlucoseBuilder setBloodSpecimenType(BloodSpecimenType bloodSpecimenType) { - if (bloodSpecimenType != null) { - bloodGlucose.setBloodSpecimenType(bloodSpecimenType); - } - return this; - } - - public BloodGlucoseBuilder setDescriptiveStatistic(DescriptiveStatistic numericDescriptor) { - bloodGlucose.setDescriptiveStatistic(numericDescriptor); - return this; - } - - public BloodGlucoseBuilder setNotes(String notes) { - bloodGlucose.setNotes(notes); - return this; - } - - public BloodGlucoseBuilder setTimeTaken(DateTime dateTime) { - bloodGlucose.getEffectiveTimeFrame().setDateTime(dateTime); - return this; - } - - @Override - public BloodGlucose build() { - return bloodGlucose; - } -} diff --git a/java-schema-objects/src/main/java/org/openmhealth/schema/pojos/build/BloodPressureBuilder.java b/java-schema-objects/src/main/java/org/openmhealth/schema/pojos/build/BloodPressureBuilder.java deleted file mode 100644 index 6d17b726..00000000 --- a/java-schema-objects/src/main/java/org/openmhealth/schema/pojos/build/BloodPressureBuilder.java +++ /dev/null @@ -1,77 +0,0 @@ -/* - * Copyright 2014 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.schema.pojos.build; - -import org.joda.time.DateTime; -import org.openmhealth.schema.pojos.*; -import org.openmhealth.schema.pojos.generic.DescriptiveStatistic; -import org.openmhealth.schema.pojos.generic.TimeFrame; - -import java.math.BigDecimal; - - -/** - * @author Danilo Bonilla - */ -public class BloodPressureBuilder implements SchemaPojoBuilder { - - private BloodPressure bloodPressure; - - public BloodPressureBuilder() { - bloodPressure = new BloodPressure(); - bloodPressure.setEffectiveTimeFrame(new TimeFrame()); - } - - public BloodPressureBuilder setValues(BigDecimal systolic, BigDecimal diastolic) { - SystolicBloodPressure systolicBloodPressure = new SystolicBloodPressure(); - systolicBloodPressure.setValue(systolic); - systolicBloodPressure.setUnit(BloodPressureUnit.mmHg); - DiastolicBloodPressure diastolicBloodPressure = new DiastolicBloodPressure(); - diastolicBloodPressure.setValue(diastolic); - diastolicBloodPressure.setUnit(BloodPressureUnit.mmHg); - bloodPressure.setDiastolic(diastolicBloodPressure); - bloodPressure.setSystolic(systolicBloodPressure); - return this; - } - - public BloodPressureBuilder setPositionDuringMeasurement( - PositionDuringMeasurement positionDuringMeasurement) { - - bloodPressure.setPositionDuringMeasurement(positionDuringMeasurement); - return this; - } - - public BloodPressureBuilder setDescriptiveStatistic(DescriptiveStatistic descriptiveStatistic) { - bloodPressure.setDescriptiveStatistic(descriptiveStatistic); - return this; - } - - public BloodPressureBuilder setNotes(String notes) { - bloodPressure.setNotes(notes); - return this; - } - - @Override - public BloodPressure build() { - return bloodPressure; - } - - public BloodPressureBuilder setTimeTaken(DateTime dateTime) { - bloodPressure.setEffectiveTimeFrame(TimeFrame.withDateTime(dateTime)); - return this; - } -} diff --git a/java-schema-objects/src/main/java/org/openmhealth/schema/pojos/build/BodyHeightBuilder.java b/java-schema-objects/src/main/java/org/openmhealth/schema/pojos/build/BodyHeightBuilder.java deleted file mode 100644 index d8783678..00000000 --- a/java-schema-objects/src/main/java/org/openmhealth/schema/pojos/build/BodyHeightBuilder.java +++ /dev/null @@ -1,64 +0,0 @@ -/* - * Copyright 2014 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.schema.pojos.build; - -import org.joda.time.DateTime; -import org.openmhealth.schema.pojos.BodyHeight; -import org.openmhealth.schema.pojos.generic.LengthUnitValue; -import org.openmhealth.schema.pojos.generic.TimeFrame; - -import java.math.BigDecimal; - - -/** - * @author Danilo Bonilla - */ -public class BodyHeightBuilder implements SchemaPojoBuilder { - - private BodyHeight bodyHeight; - - public BodyHeightBuilder() { - bodyHeight = new BodyHeight(); - bodyHeight.setEffectiveTimeFrame(new TimeFrame()); - } - - public BodyHeightBuilder setHeight(String value, String unit) { - LengthUnitValue lengthUnitValue = new LengthUnitValue(); - lengthUnitValue.setValue(new BigDecimal(value)); - lengthUnitValue.setUnit(LengthUnitValue.LengthUnit.valueOf(unit)); - bodyHeight.setLengthUnitValue(lengthUnitValue); - return this; - } - - public BodyHeightBuilder setHeight(Double value, LengthUnitValue.LengthUnit unit) { - LengthUnitValue lengthUnitValue = new LengthUnitValue(); - lengthUnitValue.setValue(new BigDecimal(value)); - lengthUnitValue.setUnit(unit); - bodyHeight.setLengthUnitValue(lengthUnitValue); - return this; - } - - public BodyHeightBuilder setTimeTaken(DateTime dateTime) { - bodyHeight.setEffectiveTimeFrame(TimeFrame.withDateTime(dateTime)); - return this; - } - - @Override - public BodyHeight build() { - return bodyHeight; - } -} diff --git a/java-schema-objects/src/main/java/org/openmhealth/schema/pojos/build/BodyWeightBuilder.java b/java-schema-objects/src/main/java/org/openmhealth/schema/pojos/build/BodyWeightBuilder.java deleted file mode 100644 index 6f9053d1..00000000 --- a/java-schema-objects/src/main/java/org/openmhealth/schema/pojos/build/BodyWeightBuilder.java +++ /dev/null @@ -1,64 +0,0 @@ -/* - * Copyright 2014 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.schema.pojos.build; - -import org.joda.time.DateTime; -import org.openmhealth.schema.pojos.BodyWeight; -import org.openmhealth.schema.pojos.generic.MassUnitValue; -import org.openmhealth.schema.pojos.generic.TimeFrame; - -import java.math.BigDecimal; - - -/** - * @author Danilo Bonilla - */ -public class BodyWeightBuilder implements SchemaPojoBuilder { - - private BodyWeight bodyWeight; - - public BodyWeightBuilder() { - bodyWeight = new BodyWeight(); - bodyWeight.setEffectiveTimeFrame(new TimeFrame()); - } - - public BodyWeightBuilder setWeight(String value, String unit) { - MassUnitValue massUnitValue = new MassUnitValue(); - massUnitValue.setValue(new BigDecimal(value)); - massUnitValue.setUnit(MassUnitValue.MassUnit.valueOf(unit)); - bodyWeight.setMassUnitValue(massUnitValue); - return this; - } - - public BodyWeightBuilder setWeight(Double value, MassUnitValue.MassUnit unit) { - MassUnitValue massUnitValue = new MassUnitValue(); - massUnitValue.setValue(new BigDecimal(value)); - massUnitValue.setUnit(unit); - bodyWeight.setMassUnitValue(massUnitValue); - return this; - } - - public BodyWeightBuilder setTimeTaken(DateTime dateTime) { - bodyWeight.setEffectiveTimeFrame(TimeFrame.withDateTime(dateTime)); - return this; - } - - @Override - public BodyWeight build() { - return bodyWeight; - } -} diff --git a/java-schema-objects/src/main/java/org/openmhealth/schema/pojos/build/HeartRateBuilder.java b/java-schema-objects/src/main/java/org/openmhealth/schema/pojos/build/HeartRateBuilder.java deleted file mode 100644 index 34a5a4fa..00000000 --- a/java-schema-objects/src/main/java/org/openmhealth/schema/pojos/build/HeartRateBuilder.java +++ /dev/null @@ -1,61 +0,0 @@ -/* - * Copyright 2014 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.schema.pojos.build; - -import org.joda.time.DateTime; -import org.openmhealth.schema.pojos.HeartRate; -import org.openmhealth.schema.pojos.HeartRateUnitValue; -import org.openmhealth.schema.pojos.TemporalRelationshipToPhysicalActivity; -import org.openmhealth.schema.pojos.generic.TimeFrame; - -import static org.openmhealth.schema.pojos.HeartRateUnitValue.Unit.bpm; - - -/** - * @author Danilo Bonilla - */ -public class HeartRateBuilder implements SchemaPojoBuilder { - - private HeartRate heartRate; - - public HeartRateBuilder() { - heartRate = new HeartRate(); - heartRate.setEffectiveTimeFrame(new TimeFrame()); - } - - public HeartRateBuilder withTimeTaken(DateTime dateTime) { - heartRate.getEffectiveTimeFrame().setDateTime(dateTime); - return this; - } - - public HeartRateBuilder withRate(Integer value) { - HeartRateUnitValue heartRateUnitValue = new HeartRateUnitValue(value, bpm); - heartRate.setHeartRate(heartRateUnitValue); - return this; - } - - public HeartRateBuilder withTimeTakenDescription( - TemporalRelationshipToPhysicalActivity description) { - heartRate.setTemporalRelationshipToPhysicalActivity(description); - return this; - } - - @Override - public HeartRate build() { - return heartRate; - } -} diff --git a/java-schema-objects/src/main/java/org/openmhealth/schema/pojos/build/SchemaPojoBuilder.java b/java-schema-objects/src/main/java/org/openmhealth/schema/pojos/build/SchemaPojoBuilder.java deleted file mode 100644 index a1a626ea..00000000 --- a/java-schema-objects/src/main/java/org/openmhealth/schema/pojos/build/SchemaPojoBuilder.java +++ /dev/null @@ -1,25 +0,0 @@ -/* - * Copyright 2014 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.schema.pojos.build; - -/** - * @author Danilo Bonilla - */ -public interface SchemaPojoBuilder { - - T build(); -} diff --git a/java-schema-objects/src/main/java/org/openmhealth/schema/pojos/build/SleepDurationBuilder.java b/java-schema-objects/src/main/java/org/openmhealth/schema/pojos/build/SleepDurationBuilder.java deleted file mode 100644 index 00657034..00000000 --- a/java-schema-objects/src/main/java/org/openmhealth/schema/pojos/build/SleepDurationBuilder.java +++ /dev/null @@ -1,60 +0,0 @@ -/* - * Copyright 2014 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.schema.pojos.build; - -import org.joda.time.DateTime; -import org.openmhealth.schema.pojos.SleepDuration; -import org.openmhealth.schema.pojos.SleepDurationUnitValue; -import org.openmhealth.schema.pojos.generic.TimeFrame; - -import java.math.BigDecimal; - - -/** - * @author Danilo Bonilla - */ -public class SleepDurationBuilder implements SchemaPojoBuilder { - - private SleepDuration sleepDuration; - - public SleepDurationBuilder() { - sleepDuration = new SleepDuration(); - } - - public SleepDurationBuilder withStartAndEndAndDuration(DateTime start, - DateTime end, - Double value, - SleepDurationUnitValue.Unit unit - ) { - sleepDuration.setEffectiveTimeFrame( - TimeFrame.withTimeInterval(start,end) - ); - sleepDuration.setSleepDurationUnitValue( - new SleepDurationUnitValue(new BigDecimal(value), unit)); - return this; - } - - public SleepDurationBuilder setNotes(String notes) { - sleepDuration.setNotes(notes); - return this; - } - - @Override - public SleepDuration build() { - return sleepDuration; - } -} diff --git a/java-schema-objects/src/main/java/org/openmhealth/schema/pojos/build/StepCountBuilder.java b/java-schema-objects/src/main/java/org/openmhealth/schema/pojos/build/StepCountBuilder.java deleted file mode 100644 index e362aa35..00000000 --- a/java-schema-objects/src/main/java/org/openmhealth/schema/pojos/build/StepCountBuilder.java +++ /dev/null @@ -1,56 +0,0 @@ -/* - * Copyright 2014 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.schema.pojos.build; - -import org.joda.time.DateTime; -import org.openmhealth.schema.pojos.StepCount; -import org.openmhealth.schema.pojos.generic.DurationUnitValue; -import org.openmhealth.schema.pojos.generic.TimeFrame; - - -/** - * @author Danilo Bonilla - */ -public class StepCountBuilder implements SchemaPojoBuilder { - - private StepCount stepCount; - - public StepCountBuilder() { - stepCount = new StepCount(); - } - - public StepCountBuilder setSteps(Integer value) { - stepCount.setStepCount(value); - return this; - } - - public StepCountBuilder withStartAndDuration(DateTime start, Double value, - DurationUnitValue.DurationUnit unit) { - stepCount.setEffectiveTimeFrame(TimeFrame.withTimeInterval(start, value, unit)); - return this; - } - - public StepCountBuilder withStartAndEnd(DateTime start, DateTime end) { - stepCount.setEffectiveTimeFrame(TimeFrame.withTimeInterval(start, end)); - return this; - } - - @Override - public StepCount build() { - return stepCount; - } -} diff --git a/java-schema-objects/src/main/java/org/openmhealth/schema/pojos/generic/DescriptiveStatistic.java b/java-schema-objects/src/main/java/org/openmhealth/schema/pojos/generic/DescriptiveStatistic.java deleted file mode 100644 index 209dc85c..00000000 --- a/java-schema-objects/src/main/java/org/openmhealth/schema/pojos/generic/DescriptiveStatistic.java +++ /dev/null @@ -1,24 +0,0 @@ -/* - * Copyright 2014 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.schema.pojos.generic; - -/** - * @author Danilo Bonilla - */ -public enum DescriptiveStatistic { - average, maximum, minimum, standard_deviation, variance, sum, median -} diff --git a/java-schema-objects/src/main/java/org/openmhealth/schema/pojos/generic/DurationUnitValue.java b/java-schema-objects/src/main/java/org/openmhealth/schema/pojos/generic/DurationUnitValue.java deleted file mode 100644 index b284cc18..00000000 --- a/java-schema-objects/src/main/java/org/openmhealth/schema/pojos/generic/DurationUnitValue.java +++ /dev/null @@ -1,57 +0,0 @@ -/* - * Copyright 2014 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.schema.pojos.generic; - -import com.fasterxml.jackson.annotation.JsonInclude; -import com.fasterxml.jackson.annotation.JsonProperty; - -import java.math.BigDecimal; - - -/** - * @author Danilo Bonilla - */ -@JsonInclude(JsonInclude.Include.NON_NULL) -public class DurationUnitValue { - - @JsonProperty(value = "value", required = true) - private BigDecimal value; - - @JsonProperty(value = "unit", required = true) - private DurationUnit unit; - - public enum DurationUnit {ps, ns, mics, ms, sec, min, h, d, wk, mo, yr} - - public DurationUnitValue() { - } - - public BigDecimal getValue() { - return value; - } - - public void setValue(BigDecimal value) { - this.value = value; - } - - public DurationUnit getUnit() { - return unit; - } - - public void setUnit(DurationUnit unit) { - this.unit = unit; - } -} diff --git a/java-schema-objects/src/main/java/org/openmhealth/schema/pojos/generic/LengthUnitValue.java b/java-schema-objects/src/main/java/org/openmhealth/schema/pojos/generic/LengthUnitValue.java deleted file mode 100644 index b55bee0a..00000000 --- a/java-schema-objects/src/main/java/org/openmhealth/schema/pojos/generic/LengthUnitValue.java +++ /dev/null @@ -1,54 +0,0 @@ -/* - * Copyright 2014 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.schema.pojos.generic; - -import com.fasterxml.jackson.annotation.JsonInclude; -import com.fasterxml.jackson.annotation.JsonProperty; - -import java.math.BigDecimal; - - -/** - * @author Danilo Bonilla - */ -@JsonInclude(JsonInclude.Include.NON_NULL) -public class LengthUnitValue { - - @JsonProperty(value = "value", required = true) - private BigDecimal value; - - @JsonProperty(value = "unit", required = true) - private LengthUnit unit; - - public enum LengthUnit {fm, pm, nm, micg, mm, cm, m, km, in, ft, yd, mi} - - public BigDecimal getValue() { - return value; - } - - public void setValue(BigDecimal value) { - this.value = value; - } - - public LengthUnit getUnit() { - return unit; - } - - public void setUnit(LengthUnit unit) { - this.unit = unit; - } -} diff --git a/java-schema-objects/src/main/java/org/openmhealth/schema/pojos/generic/MassUnitValue.java b/java-schema-objects/src/main/java/org/openmhealth/schema/pojos/generic/MassUnitValue.java deleted file mode 100644 index 7b9606e1..00000000 --- a/java-schema-objects/src/main/java/org/openmhealth/schema/pojos/generic/MassUnitValue.java +++ /dev/null @@ -1,58 +0,0 @@ -/* - * Copyright 2014 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.schema.pojos.generic; - - -import com.fasterxml.jackson.annotation.JsonInclude; -import com.fasterxml.jackson.annotation.JsonProperty; - -import java.math.BigDecimal; - - -/** - * @author Danilo Bonilla - */ -@JsonInclude(JsonInclude.Include.NON_NULL) -public class MassUnitValue { - - @JsonProperty(value = "value", required = true) - private BigDecimal value; - - @JsonProperty(value = "unit", required = true) - private MassUnit unit; - - public enum MassUnit {fg, pg, ng, micg, mg, g, kg, metric_ton, gr, oz, lb, ton} - - public MassUnitValue() { - } - - public BigDecimal getValue() { - return value; - } - - public void setValue(BigDecimal value) { - this.value = value; - } - - public MassUnit getUnit() { - return unit; - } - - public void setUnit(MassUnit unit) { - this.unit = unit; - } -} diff --git a/java-schema-objects/src/main/java/org/openmhealth/schema/pojos/generic/PartOfDay.java b/java-schema-objects/src/main/java/org/openmhealth/schema/pojos/generic/PartOfDay.java deleted file mode 100644 index 5b1bf1a2..00000000 --- a/java-schema-objects/src/main/java/org/openmhealth/schema/pojos/generic/PartOfDay.java +++ /dev/null @@ -1,24 +0,0 @@ -/* - * Copyright 2014 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.schema.pojos.generic; - -/** - * @author Danilo Bonilla - */ -public enum PartOfDay { - morning, afternoon, evening, night -} diff --git a/java-schema-objects/src/main/java/org/openmhealth/schema/pojos/generic/TimeFrame.java b/java-schema-objects/src/main/java/org/openmhealth/schema/pojos/generic/TimeFrame.java deleted file mode 100644 index ee5def90..00000000 --- a/java-schema-objects/src/main/java/org/openmhealth/schema/pojos/generic/TimeFrame.java +++ /dev/null @@ -1,106 +0,0 @@ -/* - * Copyright 2014 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.schema.pojos.generic; - -import com.fasterxml.jackson.annotation.JsonIgnore; -import com.fasterxml.jackson.annotation.JsonInclude; -import com.fasterxml.jackson.annotation.JsonProperty; -import com.fasterxml.jackson.databind.annotation.JsonDeserialize; -import com.fasterxml.jackson.databind.annotation.JsonSerialize; -import org.joda.time.DateTime; -import org.openmhealth.schema.pojos.serialize.dates.ISODateDeserializer; -import org.openmhealth.schema.pojos.serialize.dates.ISODateSerializer; - -import java.math.BigDecimal; - - -/** - * @author Danilo Bonilla - */ -@JsonInclude(JsonInclude.Include.NON_NULL) -public class TimeFrame { - - @JsonProperty(value = "date_time", required = false) - @JsonSerialize(using = ISODateSerializer.class) - @JsonDeserialize(using = ISODateDeserializer.class) - private DateTime dateTime; - - @JsonProperty(value = "time_interval", required = false) - private TimeInterval timeInterval; - - public TimeFrame() { - } - - @JsonIgnore - public DateTime getTimestamp() { - if (dateTime != null) { - return dateTime; - } - if (timeInterval != null && timeInterval.getDate() != null) { - return timeInterval.getDate(); - } - if (timeInterval != null && timeInterval.getStartTime() != null) { - return timeInterval.getStartTime(); - } - return null; - } - - public DateTime getDateTime() { - return dateTime; - } - - public void setDateTime(DateTime dateTime) { - this.dateTime = dateTime; - } - - public TimeInterval getTimeInterval() { - return timeInterval; - } - - public void setTimeInterval(TimeInterval timeInterval) { - this.timeInterval = timeInterval; - } - - public static TimeFrame withDateTime(DateTime dateTime) { - TimeFrame timeFrame = new TimeFrame(); - timeFrame.setDateTime(dateTime); - return timeFrame; - } - - public static TimeFrame withTimeInterval(DateTime startTime, DateTime endTime) { - TimeFrame timeFrame = new TimeFrame(); - TimeInterval interval = new TimeInterval(); - interval.setStartTime(startTime); - interval.setEndTime(endTime); - timeFrame.setTimeInterval(interval); - return timeFrame; - } - - public static TimeFrame withTimeInterval(DateTime startTime, - Double durationValue, - DurationUnitValue.DurationUnit unit) { - TimeFrame timeFrame = new TimeFrame(); - TimeInterval interval = new TimeInterval(); - interval.setStartTime(startTime); - DurationUnitValue durationUnitValue = new DurationUnitValue(); - durationUnitValue.setUnit(unit); - durationUnitValue.setValue(new BigDecimal(durationValue)); - interval.setDuration(durationUnitValue); - timeFrame.setTimeInterval(interval); - return timeFrame; - } -} diff --git a/java-schema-objects/src/main/java/org/openmhealth/schema/pojos/generic/TimeInterval.java b/java-schema-objects/src/main/java/org/openmhealth/schema/pojos/generic/TimeInterval.java deleted file mode 100644 index 9ae48568..00000000 --- a/java-schema-objects/src/main/java/org/openmhealth/schema/pojos/generic/TimeInterval.java +++ /dev/null @@ -1,120 +0,0 @@ -/* - * Copyright 2014 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.schema.pojos.generic; - - -import com.fasterxml.jackson.annotation.JsonInclude; -import com.fasterxml.jackson.annotation.JsonProperty; -import com.fasterxml.jackson.databind.annotation.JsonDeserialize; -import com.fasterxml.jackson.databind.annotation.JsonSerialize; -import org.joda.time.DateTime; -import org.openmhealth.schema.pojos.serialize.dates.ISODateDeserializer; -import org.openmhealth.schema.pojos.serialize.dates.ISODateSerializer; -import org.openmhealth.schema.pojos.serialize.dates.SimpleDateDeserializer; -import org.openmhealth.schema.pojos.serialize.dates.SimpleDateSerializer; - -import java.math.BigDecimal; - - -/** - * @author Danilo Bonilla - */ -@JsonInclude(JsonInclude.Include.NON_NULL) -public class TimeInterval { - - public final static String FULLDATE_FORMAT = "yyyy-MM-dd"; - - @JsonProperty(value = "date", required = false) - @JsonSerialize(using = SimpleDateSerializer.class) - @JsonDeserialize(using = SimpleDateDeserializer.class) - private DateTime date; - - @JsonProperty(value = "start_date_time", required = false) - @JsonSerialize(using = ISODateSerializer.class) - @JsonDeserialize(using = ISODateDeserializer.class) - private DateTime startTime; - - @JsonProperty(value = "end_date_time", required = false) - @JsonSerialize(using = ISODateSerializer.class) - @JsonDeserialize(using = ISODateDeserializer.class) - private DateTime endTime; - - @JsonProperty(value = "duration", required = false) - private DurationUnitValue duration; - - @JsonProperty(value = "part_of_day", required = false) - private PartOfDay partOfDay; - - public DateTime getDate() { - return date; - } - - public void setDate(DateTime date) { - this.date = date; - } - - public DateTime getStartTime() { - return startTime; - } - - public void setStartTime(DateTime startTime) { - this.startTime = startTime; - } - - public DateTime getEndTime() { - return endTime; - } - - public void setEndTime(DateTime endTime) { - this.endTime = endTime; - } - - public DurationUnitValue getDuration() { - return duration; - } - - public void setDuration(DurationUnitValue duration) { - this.duration = duration; - } - - public PartOfDay getPartOfDay() { - return partOfDay; - } - - public void setPartOfDay(PartOfDay partOfDay) { - this.partOfDay = partOfDay; - } - - public static TimeInterval withStartAndEnd(DateTime start, DateTime end) { - TimeInterval interval = new TimeInterval(); - interval.setStartTime(start); - interval.setEndTime(end); - return interval; - } - - public static TimeInterval withStartAndDuration(DateTime startTime, - Double durationValue, - DurationUnitValue.DurationUnit unit) { - TimeInterval interval = new TimeInterval(); - interval.setStartTime(startTime); - DurationUnitValue durationUnitValue = new DurationUnitValue(); - durationUnitValue.setUnit(unit); - durationUnitValue.setValue(new BigDecimal(durationValue)); - interval.setDuration(durationUnitValue); - return interval; - } -} diff --git a/java-schema-objects/src/main/java/org/openmhealth/schema/pojos/serialize/BloodGlucoseUnitValueDeserializer.java b/java-schema-objects/src/main/java/org/openmhealth/schema/pojos/serialize/BloodGlucoseUnitValueDeserializer.java deleted file mode 100644 index e0409db5..00000000 --- a/java-schema-objects/src/main/java/org/openmhealth/schema/pojos/serialize/BloodGlucoseUnitValueDeserializer.java +++ /dev/null @@ -1,43 +0,0 @@ -/* - * Copyright 2014 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.schema.pojos.serialize; - -import com.fasterxml.jackson.core.JsonParser; -import com.fasterxml.jackson.core.ObjectCodec; -import com.fasterxml.jackson.databind.DeserializationContext; -import com.fasterxml.jackson.databind.JsonDeserializer; -import com.fasterxml.jackson.databind.JsonNode; -import org.openmhealth.schema.pojos.BloodGlucoseUnitValue; - -import java.io.IOException; - - -/** - * @author Danilo Bonilla - */ -public class BloodGlucoseUnitValueDeserializer extends JsonDeserializer { - - @Override - public BloodGlucoseUnitValue.Unit deserialize( - JsonParser jsonParser, DeserializationContext deserializationContext) - throws IOException { - - ObjectCodec oc = jsonParser.getCodec(); - JsonNode node = oc.readTree(jsonParser); - return BloodGlucoseUnitValue.Unit.valueForLabel(node.asText()); - } -} diff --git a/java-schema-objects/src/main/java/org/openmhealth/schema/pojos/serialize/BloodSpecimenTypeDeserializer.java b/java-schema-objects/src/main/java/org/openmhealth/schema/pojos/serialize/BloodSpecimenTypeDeserializer.java deleted file mode 100644 index 03ebbb3f..00000000 --- a/java-schema-objects/src/main/java/org/openmhealth/schema/pojos/serialize/BloodSpecimenTypeDeserializer.java +++ /dev/null @@ -1,43 +0,0 @@ -/* - * Copyright 2014 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.schema.pojos.serialize; - -import com.fasterxml.jackson.core.JsonParser; -import com.fasterxml.jackson.core.ObjectCodec; -import com.fasterxml.jackson.databind.DeserializationContext; -import com.fasterxml.jackson.databind.JsonDeserializer; -import com.fasterxml.jackson.databind.JsonNode; -import org.openmhealth.schema.pojos.BloodSpecimenType; - -import java.io.IOException; - - -/** - * @author Danilo Bonilla - */ -public class BloodSpecimenTypeDeserializer extends JsonDeserializer { - - @Override - public BloodSpecimenType deserialize(JsonParser jsonParser, - DeserializationContext deserializationContext) - throws IOException { - - ObjectCodec oc = jsonParser.getCodec(); - JsonNode node = oc.readTree(jsonParser); - return BloodSpecimenType.valueForLabel(node.asText()); - } -} diff --git a/java-schema-objects/src/main/java/org/openmhealth/schema/pojos/serialize/HeartRateUnitDeserializer.java b/java-schema-objects/src/main/java/org/openmhealth/schema/pojos/serialize/HeartRateUnitDeserializer.java deleted file mode 100644 index b814420b..00000000 --- a/java-schema-objects/src/main/java/org/openmhealth/schema/pojos/serialize/HeartRateUnitDeserializer.java +++ /dev/null @@ -1,41 +0,0 @@ -/* - * Copyright 2014 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.schema.pojos.serialize; - -import com.fasterxml.jackson.core.JsonParser; -import com.fasterxml.jackson.core.ObjectCodec; -import com.fasterxml.jackson.databind.DeserializationContext; -import com.fasterxml.jackson.databind.JsonDeserializer; -import com.fasterxml.jackson.databind.JsonNode; -import org.openmhealth.schema.pojos.HeartRateUnitValue; - -import java.io.IOException; - - -/** - * @author Danilo Bonilla - */ -public class HeartRateUnitDeserializer extends JsonDeserializer { - - @Override - public HeartRateUnitValue.Unit deserialize(JsonParser jsonParser, DeserializationContext deserializationContext) - throws IOException { - ObjectCodec oc = jsonParser.getCodec(); - JsonNode node = oc.readTree(jsonParser); - return HeartRateUnitValue.Unit.valueForLabel(node.asText()); - } -} diff --git a/java-schema-objects/src/main/java/org/openmhealth/schema/pojos/serialize/LabeledEnumSerializer.java b/java-schema-objects/src/main/java/org/openmhealth/schema/pojos/serialize/LabeledEnumSerializer.java deleted file mode 100644 index 84746655..00000000 --- a/java-schema-objects/src/main/java/org/openmhealth/schema/pojos/serialize/LabeledEnumSerializer.java +++ /dev/null @@ -1,39 +0,0 @@ -/* - * Copyright 2014 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.schema.pojos.serialize; - - -import com.fasterxml.jackson.core.JsonGenerator; -import com.fasterxml.jackson.databind.JsonSerializer; -import com.fasterxml.jackson.databind.SerializerProvider; -import org.openmhealth.schema.pojos.LabeledEnum; - -import java.io.IOException; - - -/** - * @author Danilo Bonilla - */ -public class LabeledEnumSerializer extends JsonSerializer { - - @Override - public void serialize(LabeledEnum labeledEnum, - JsonGenerator jsonGenerator, - SerializerProvider serializerProvider) throws IOException { - jsonGenerator.writeString(labeledEnum.getLabel()); - } -} diff --git a/java-schema-objects/src/main/java/org/openmhealth/schema/pojos/serialize/PositionDuringMeasurementDeserializer.java b/java-schema-objects/src/main/java/org/openmhealth/schema/pojos/serialize/PositionDuringMeasurementDeserializer.java deleted file mode 100644 index 12445e85..00000000 --- a/java-schema-objects/src/main/java/org/openmhealth/schema/pojos/serialize/PositionDuringMeasurementDeserializer.java +++ /dev/null @@ -1,44 +0,0 @@ -/* - * Copyright 2014 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.schema.pojos.serialize; - -import com.fasterxml.jackson.core.JsonParser; -import com.fasterxml.jackson.core.ObjectCodec; -import com.fasterxml.jackson.databind.DeserializationContext; -import com.fasterxml.jackson.databind.JsonDeserializer; -import com.fasterxml.jackson.databind.JsonNode; -import org.openmhealth.schema.pojos.PositionDuringMeasurement; - -import java.io.IOException; - - -/** - * @author Danilo Bonilla - */ -public class PositionDuringMeasurementDeserializer - extends JsonDeserializer { - - @Override - public PositionDuringMeasurement deserialize( - JsonParser jsonParser, - DeserializationContext deserializationContext) throws IOException { - - ObjectCodec oc = jsonParser.getCodec(); - JsonNode node = oc.readTree(jsonParser); - return PositionDuringMeasurement.valueForLabel(node.asText()); - } -} diff --git a/java-schema-objects/src/main/java/org/openmhealth/schema/pojos/serialize/TemporalRelationshipToPhysicalActivityDeserializer.java b/java-schema-objects/src/main/java/org/openmhealth/schema/pojos/serialize/TemporalRelationshipToPhysicalActivityDeserializer.java deleted file mode 100644 index 19b3a7f0..00000000 --- a/java-schema-objects/src/main/java/org/openmhealth/schema/pojos/serialize/TemporalRelationshipToPhysicalActivityDeserializer.java +++ /dev/null @@ -1,43 +0,0 @@ -/* - * Copyright 2014 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.schema.pojos.serialize; - - -import com.fasterxml.jackson.core.JsonParser; -import com.fasterxml.jackson.core.ObjectCodec; -import com.fasterxml.jackson.databind.DeserializationContext; -import com.fasterxml.jackson.databind.JsonDeserializer; -import com.fasterxml.jackson.databind.JsonNode; -import org.openmhealth.schema.pojos.TemporalRelationshipToPhysicalActivity; - -import java.io.IOException; - - -/** - * @author Danilo Bonilla - */ -public class TemporalRelationshipToPhysicalActivityDeserializer - extends JsonDeserializer { - - @Override - public TemporalRelationshipToPhysicalActivity deserialize( - JsonParser jsonParser, DeserializationContext deserializationContext) throws IOException { - ObjectCodec oc = jsonParser.getCodec(); - JsonNode node = oc.readTree(jsonParser); - return TemporalRelationshipToPhysicalActivity.valueForLabel(node.asText()); - } -} diff --git a/java-schema-objects/src/main/java/org/openmhealth/schema/pojos/serialize/dates/ISODateDeserializer.java b/java-schema-objects/src/main/java/org/openmhealth/schema/pojos/serialize/dates/ISODateDeserializer.java deleted file mode 100644 index 4f47232f..00000000 --- a/java-schema-objects/src/main/java/org/openmhealth/schema/pojos/serialize/dates/ISODateDeserializer.java +++ /dev/null @@ -1,46 +0,0 @@ -/* - * Copyright 2014 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.schema.pojos.serialize.dates; - -import com.fasterxml.jackson.core.JsonParser; -import com.fasterxml.jackson.core.ObjectCodec; -import com.fasterxml.jackson.databind.DeserializationContext; -import com.fasterxml.jackson.databind.JsonDeserializer; -import com.fasterxml.jackson.databind.JsonNode; -import org.joda.time.DateTime; -import org.joda.time.format.DateTimeFormat; -import org.joda.time.format.DateTimeFormatter; - -import java.io.IOException; - - -/** - * @author Danilo Bonilla - */ -public class ISODateDeserializer extends JsonDeserializer { - - protected static DateTimeFormatter formatter = - DateTimeFormat.forPattern("yyyy-MM-dd'T'HH:mm:ss.SSSZ"); - - @Override - public DateTime deserialize(JsonParser jsonParser, DeserializationContext ctxt) - throws IOException { - ObjectCodec oc = jsonParser.getCodec(); - JsonNode node = oc.readTree(jsonParser); - return formatter.parseDateTime(node.asText()); - } -} diff --git a/java-schema-objects/src/main/java/org/openmhealth/schema/pojos/serialize/dates/ISODateSerializer.java b/java-schema-objects/src/main/java/org/openmhealth/schema/pojos/serialize/dates/ISODateSerializer.java deleted file mode 100644 index 439fd0f5..00000000 --- a/java-schema-objects/src/main/java/org/openmhealth/schema/pojos/serialize/dates/ISODateSerializer.java +++ /dev/null @@ -1,45 +0,0 @@ -/* - * Copyright 2014 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.schema.pojos.serialize.dates; - -import com.fasterxml.jackson.core.JsonGenerator; -import com.fasterxml.jackson.databind.JsonSerializer; -import com.fasterxml.jackson.databind.SerializerProvider; -import org.joda.time.DateTime; -import org.joda.time.format.DateTimeFormatter; -import org.joda.time.format.ISODateTimeFormat; - -import java.io.IOException; - - -/** - * Standard date serializer for Joda time in the regular schemas. - * - * @author Danilo Bonilla - */ -public class ISODateSerializer extends JsonSerializer { - - protected static DateTimeFormatter formatter = ISODateTimeFormat.dateTime(); - - @Override - public void serialize(DateTime value, JsonGenerator gen, - SerializerProvider arg2) - throws IOException { - gen.writeString(formatter.print(value)); - } - -} diff --git a/java-schema-objects/src/main/java/org/openmhealth/schema/pojos/serialize/dates/MetadataTimestampDeserializer.java b/java-schema-objects/src/main/java/org/openmhealth/schema/pojos/serialize/dates/MetadataTimestampDeserializer.java deleted file mode 100644 index 964948dd..00000000 --- a/java-schema-objects/src/main/java/org/openmhealth/schema/pojos/serialize/dates/MetadataTimestampDeserializer.java +++ /dev/null @@ -1,32 +0,0 @@ -/* - * Copyright 2014 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.schema.pojos.serialize.dates; - -import org.joda.time.format.DateTimeFormat; -import org.joda.time.format.DateTimeFormatter; -import org.openmhealth.schema.pojos.Metadata; - - -/** - * @author Danilo Bonilla - */ -@Deprecated -public class MetadataTimestampDeserializer extends ISODateDeserializer{ - - protected static DateTimeFormatter formatter = - DateTimeFormat.forPattern(Metadata.TIMESTAMP_FORMAT); -} diff --git a/java-schema-objects/src/main/java/org/openmhealth/schema/pojos/serialize/dates/MetadataTimestampSerializer.java b/java-schema-objects/src/main/java/org/openmhealth/schema/pojos/serialize/dates/MetadataTimestampSerializer.java deleted file mode 100644 index 383b896a..00000000 --- a/java-schema-objects/src/main/java/org/openmhealth/schema/pojos/serialize/dates/MetadataTimestampSerializer.java +++ /dev/null @@ -1,32 +0,0 @@ -/* - * Copyright 2014 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.schema.pojos.serialize.dates; - -import org.joda.time.format.DateTimeFormat; -import org.joda.time.format.DateTimeFormatter; -import org.openmhealth.schema.pojos.Metadata; - - -/** - * @author Danilo Bonilla - */ -@Deprecated -public class MetadataTimestampSerializer extends ISODateSerializer{ - - protected static DateTimeFormatter formatter = - DateTimeFormat.forPattern(Metadata.TIMESTAMP_FORMAT); -} \ No newline at end of file diff --git a/java-schema-objects/src/main/java/org/openmhealth/schema/pojos/serialize/dates/SimpleDateDeserializer.java b/java-schema-objects/src/main/java/org/openmhealth/schema/pojos/serialize/dates/SimpleDateDeserializer.java deleted file mode 100644 index d647240e..00000000 --- a/java-schema-objects/src/main/java/org/openmhealth/schema/pojos/serialize/dates/SimpleDateDeserializer.java +++ /dev/null @@ -1,30 +0,0 @@ -/* - * Copyright 2014 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.schema.pojos.serialize.dates; - -import org.joda.time.format.DateTimeFormat; -import org.joda.time.format.DateTimeFormatter; -import org.openmhealth.schema.pojos.generic.TimeInterval; - - -/** - * @author Danilo Bonilla - */ -public class SimpleDateDeserializer extends ISODateDeserializer { - protected static DateTimeFormatter formatter = - DateTimeFormat.forPattern(TimeInterval.FULLDATE_FORMAT); -} diff --git a/java-schema-objects/src/main/java/org/openmhealth/schema/pojos/serialize/dates/SimpleDateSerializer.java b/java-schema-objects/src/main/java/org/openmhealth/schema/pojos/serialize/dates/SimpleDateSerializer.java deleted file mode 100644 index 595da4a4..00000000 --- a/java-schema-objects/src/main/java/org/openmhealth/schema/pojos/serialize/dates/SimpleDateSerializer.java +++ /dev/null @@ -1,30 +0,0 @@ -/* - * Copyright 2014 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.schema.pojos.serialize.dates; - -import org.joda.time.format.DateTimeFormat; -import org.joda.time.format.DateTimeFormatter; -import org.openmhealth.schema.pojos.generic.TimeInterval; - - -/** - * @author Danilo Bonilla - */ -public class SimpleDateSerializer extends ISODateSerializer { - protected static DateTimeFormatter formatter = - DateTimeFormat.forPattern(TimeInterval.FULLDATE_FORMAT); -} diff --git a/java-schema-objects/src/test/java/org/openmhealth/schema/pojos/build/ActivityBuilderTest.java b/java-schema-objects/src/test/java/org/openmhealth/schema/pojos/build/ActivityBuilderTest.java deleted file mode 100644 index 62090c4f..00000000 --- a/java-schema-objects/src/test/java/org/openmhealth/schema/pojos/build/ActivityBuilderTest.java +++ /dev/null @@ -1,88 +0,0 @@ -/* - * Copyright 2014 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.schema.pojos.build; - -import com.fasterxml.jackson.databind.ObjectMapper; -import com.github.fge.jsonschema.core.exceptions.ProcessingException; -import com.github.fge.jsonschema.core.report.ProcessingReport; -import com.github.fge.jsonschema.main.JsonSchema; -import com.github.fge.jsonschema.main.JsonSchemaFactory; -import org.joda.time.DateTime; -import org.junit.Test; -import org.openmhealth.schema.pojos.Activity; -import org.openmhealth.schema.pojos.generic.DurationUnitValue; -import org.openmhealth.schema.pojos.generic.LengthUnitValue; - -import java.io.IOException; - -import static org.junit.Assert.*; - -/** - * @author Danilo Bonilla - */ -public class ActivityBuilderTest { - - @Test - @SuppressWarnings("unchecked") - public void testParse() throws IOException, ProcessingException { - - final String PHYSICAL_ACTIVITY_SCHEMA = "http://www.openmhealth.org/schema/omh/clinical/physical-activity-1.0.json"; - - ObjectMapper mapper = new ObjectMapper(); - - final JsonSchemaFactory factory = JsonSchemaFactory.byDefault(); - final JsonSchema schema = factory.getJsonSchema(PHYSICAL_ACTIVITY_SCHEMA); - - ProcessingReport report; - - ActivityBuilder builder = new ActivityBuilder(); - - Activity invalidActivity = builder.build(); - String invalidJson = mapper.writeValueAsString(invalidActivity); - - report = schema.validate(mapper.readTree(invalidJson)); - System.out.println(report); - assertFalse("Expected invalid result but got success", report.isSuccess()); - - Activity activity = builder - .withStartAndDuration( - new DateTime(), 3100d, DurationUnitValue.DurationUnit.sec) - .setReportedActivityIntensity(Activity.ActivityIntensity.moderate) - .setActivityName("snow boarding") - .setDistance(5d, LengthUnitValue.LengthUnit.mi).build(); - - DateTime timeStamp = activity.getTimeStamp(); - assertNotNull(timeStamp); - - String rawJson = mapper.writeValueAsString(activity); - - Activity deserialized = mapper.readValue(rawJson, Activity.class); - - assertNotNull(deserialized.getEffectiveTimeFrame().getTimeInterval().getStartTime()); - assertNotNull(deserialized.getEffectiveTimeFrame().getTimeInterval().getDuration()); - assertNotNull(deserialized.getActivityName()); - assertNotNull(deserialized.getDistance().getUnit()); - assertNotNull(deserialized.getDistance().getValue()); - assertNotNull(deserialized.getReportedActivityIntensity()); - - report = schema.validate(mapper.readTree(rawJson)); - System.out.println(report); - - assertTrue("Expected valid result!", report.isSuccess()); - } - -} diff --git a/java-schema-objects/src/test/java/org/openmhealth/schema/pojos/build/BloodGlucoseBuilderTest.java b/java-schema-objects/src/test/java/org/openmhealth/schema/pojos/build/BloodGlucoseBuilderTest.java deleted file mode 100644 index 0647c5a6..00000000 --- a/java-schema-objects/src/test/java/org/openmhealth/schema/pojos/build/BloodGlucoseBuilderTest.java +++ /dev/null @@ -1,91 +0,0 @@ -/* - * Copyright 2014 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.schema.pojos.build; - -import com.fasterxml.jackson.databind.ObjectMapper; -import com.github.fge.jsonschema.core.exceptions.ProcessingException; -import com.github.fge.jsonschema.core.report.ProcessingReport; -import com.github.fge.jsonschema.main.JsonSchema; -import com.github.fge.jsonschema.main.JsonSchemaFactory; -import org.joda.time.DateTime; -import org.junit.Test; -import org.openmhealth.schema.pojos.BloodGlucose; -import org.openmhealth.schema.pojos.BloodSpecimenType; -import org.openmhealth.schema.pojos.generic.DescriptiveStatistic; - -import java.io.IOException; -import java.math.BigDecimal; - -import static org.junit.Assert.*; - -/** - * @author Danilo Bonilla - */ -public class BloodGlucoseBuilderTest { - - @Test - @SuppressWarnings("unchecked") - public void testParse() throws IOException, ProcessingException { - //final String BLOOD_GLUCOSE_SCHEMA = "schemas/blood-glucose-1.0.json"; - final String BLOOD_GLUCOSE_SCHEMA = "http://www.openmhealth.org/schema/omh/clinical/blood-glucose-1.0.json"; - - //URL url = Thread.currentThread().getContextClassLoader().getResource(BLOOD_GLUCOSE_SCHEMA); - //assertNotNull(url); - - ObjectMapper mapper = new ObjectMapper(); - - //InputStream inputStream = url.openStream(); - // JsonNode schemaNode = mapper.readTree(inputStream); - - final JsonSchemaFactory factory = JsonSchemaFactory.byDefault(); - //final JsonSchema schema = factory.getJsonSchema(schemaNode); - final JsonSchema schema = factory.getJsonSchema(BLOOD_GLUCOSE_SCHEMA); - - ProcessingReport report; - - BloodGlucoseBuilder builder = new BloodGlucoseBuilder(); - - BloodGlucose invalidBloodGlucose = builder.build(); - String invalidJson = mapper.writeValueAsString(invalidBloodGlucose); - - report = schema.validate(mapper.readTree(invalidJson)); - System.out.println(report); - assertFalse("Expected invalid result but got success", report.isSuccess()); - - BloodGlucose bloodGlucose = builder - .setBloodSpecimenType(BloodSpecimenType.whole_blood) - .setMgdLValue(new BigDecimal(90d)) - .setNotes("yo yo yo") - .setTimeTaken(new DateTime()) - .setDescriptiveStatistic(DescriptiveStatistic.average).build(); - - String rawJson = mapper.writeValueAsString(bloodGlucose); - - BloodGlucose deserialized = mapper.readValue(rawJson, BloodGlucose.class); - - assertNotNull(deserialized.getEffectiveTimeFrame()); - assertNotNull(deserialized.getBloodGlucose().getUnit()); - assertNotNull(deserialized.getBloodGlucose().getValue()); - assertNotNull(deserialized.getNotes()); - assertNotNull(deserialized.getDescriptiveStatistic()); - - report = schema.validate(mapper.readTree(rawJson)); - System.out.println(report); - - assertTrue("Expected valid result!", report.isSuccess()); - } -} diff --git a/java-schema-objects/src/test/java/org/openmhealth/schema/pojos/build/BloodPressureBuilderTest.java b/java-schema-objects/src/test/java/org/openmhealth/schema/pojos/build/BloodPressureBuilderTest.java deleted file mode 100644 index 34234292..00000000 --- a/java-schema-objects/src/test/java/org/openmhealth/schema/pojos/build/BloodPressureBuilderTest.java +++ /dev/null @@ -1,86 +0,0 @@ -/* - * Copyright 2014 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.schema.pojos.build; - -import com.fasterxml.jackson.databind.ObjectMapper; -import com.github.fge.jsonschema.core.exceptions.ProcessingException; -import com.github.fge.jsonschema.core.report.ProcessingReport; -import com.github.fge.jsonschema.main.JsonSchema; -import com.github.fge.jsonschema.main.JsonSchemaFactory; -import org.joda.time.DateTime; -import org.junit.Test; -import org.openmhealth.schema.pojos.BloodPressure; -import org.openmhealth.schema.pojos.PositionDuringMeasurement; -import org.openmhealth.schema.pojos.generic.DescriptiveStatistic; - -import java.io.IOException; -import java.math.BigDecimal; - -import static org.junit.Assert.*; - -/** - * @author Danilo Bonilla - */ -public class BloodPressureBuilderTest { - - @Test - @SuppressWarnings("unchecked") - public void testParse() throws IOException, ProcessingException { - - final String BLOOD_PRESSURE_SCHEMA = "http://www.openmhealth.org/schema/omh/clinical/blood-pressure-1.0.json"; - - ObjectMapper mapper = new ObjectMapper(); - - final JsonSchemaFactory factory = JsonSchemaFactory.byDefault(); - final JsonSchema schema = factory.getJsonSchema(BLOOD_PRESSURE_SCHEMA); - - ProcessingReport report; - - BloodPressureBuilder builder = new BloodPressureBuilder(); - - BloodPressure invalidBloodPressure = builder.build(); - String invalidJson = mapper.writeValueAsString(invalidBloodPressure); - - report = schema.validate(mapper.readTree(invalidJson)); - System.out.println(report); - assertFalse("Expected invalid result but got success", report.isSuccess()); - - BloodPressure bloodPressure = builder - .setValues(new BigDecimal(120d), new BigDecimal(80d)) - .setNotes("yo yo yo") - .setTimeTaken(new DateTime()) - .setPositionDuringMeasurement(PositionDuringMeasurement.lying_down) - .setDescriptiveStatistic(DescriptiveStatistic.average).build(); - - String rawJson = mapper.writeValueAsString(bloodPressure); - - BloodPressure deserialized = mapper.readValue(rawJson, BloodPressure.class); - - assertNotNull(deserialized.getEffectiveTimeFrame()); - assertNotNull(deserialized.getDiastolic()); - assertNotNull(deserialized.getSystolic()); - assertNotNull(deserialized.getNotes()); - assertNotNull(deserialized.getPositionDuringMeasurement()); - assertNotNull(deserialized.getDescriptiveStatistic()); - - report = schema.validate(mapper.readTree(rawJson)); - System.out.println(report); - - assertTrue("Expected valid result!", report.isSuccess()); - } - -} diff --git a/java-schema-objects/src/test/java/org/openmhealth/schema/pojos/build/BodyHeightBuilderTest.java b/java-schema-objects/src/test/java/org/openmhealth/schema/pojos/build/BodyHeightBuilderTest.java deleted file mode 100644 index b84d0b0e..00000000 --- a/java-schema-objects/src/test/java/org/openmhealth/schema/pojos/build/BodyHeightBuilderTest.java +++ /dev/null @@ -1,76 +0,0 @@ -/* - * Copyright 2014 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.schema.pojos.build; - -import com.fasterxml.jackson.databind.ObjectMapper; -import com.github.fge.jsonschema.core.exceptions.ProcessingException; -import com.github.fge.jsonschema.core.report.ProcessingReport; -import com.github.fge.jsonschema.main.JsonSchema; -import com.github.fge.jsonschema.main.JsonSchemaFactory; -import org.joda.time.DateTime; -import org.junit.Test; -import org.openmhealth.schema.pojos.BodyHeight; - -import java.io.IOException; -import java.math.BigDecimal; - -import static org.junit.Assert.*; -import static org.openmhealth.schema.pojos.generic.LengthUnitValue.LengthUnit.cm; - -/** - * @author Danilo Bonilla - */ -public class BodyHeightBuilderTest { - - @Test - public void test() throws IOException, ProcessingException { - - final String BODY_HEIGHT_SCHEMA = "http://www.openmhealth.org/schema/omh/clinical/body-height-1.0.json"; - - ObjectMapper mapper = new ObjectMapper(); - - final JsonSchemaFactory factory = JsonSchemaFactory.byDefault(); - final JsonSchema schema = factory.getJsonSchema(BODY_HEIGHT_SCHEMA); - - ProcessingReport report; - - BodyHeightBuilder builder = new BodyHeightBuilder(); - - BodyHeight invalidBodyHeight = builder.build(); - String invalidJson = mapper.writeValueAsString(invalidBodyHeight); - - report = schema.validate(mapper.readTree(invalidJson)); - System.out.println(report); - assertFalse("Expected invalid result but got success", report.isSuccess()); - - builder.setHeight(150d, cm); - builder.setTimeTaken(new DateTime()); - BodyHeight bodyHeight = builder.build(); - - assertNotNull(bodyHeight.getEffectiveTimeFrame()); - assertNotNull(bodyHeight.getLengthUnitValue()); - assertEquals(bodyHeight.getLengthUnitValue().getUnit(), cm); - assertEquals(bodyHeight.getLengthUnitValue().getValue(), new BigDecimal(150d)); - - String rawJson = mapper.writeValueAsString(bodyHeight); - - report = schema.validate(mapper.readTree(rawJson)); - System.out.println(report); - - assertTrue("Expected valid result!", report.isSuccess()); - } -} diff --git a/java-schema-objects/src/test/java/org/openmhealth/schema/pojos/build/BodyWeightBuilderTest.java b/java-schema-objects/src/test/java/org/openmhealth/schema/pojos/build/BodyWeightBuilderTest.java deleted file mode 100644 index ed9e067a..00000000 --- a/java-schema-objects/src/test/java/org/openmhealth/schema/pojos/build/BodyWeightBuilderTest.java +++ /dev/null @@ -1,79 +0,0 @@ -/* - * Copyright 2014 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.schema.pojos.build; - -import com.fasterxml.jackson.databind.ObjectMapper; -import com.github.fge.jsonschema.core.exceptions.ProcessingException; -import com.github.fge.jsonschema.core.report.ProcessingReport; -import com.github.fge.jsonschema.main.JsonSchema; -import com.github.fge.jsonschema.main.JsonSchemaFactory; -import org.joda.time.DateTime; -import org.junit.Test; -import org.openmhealth.schema.pojos.BodyWeight; - -import java.io.IOException; -import java.math.BigDecimal; - -import static org.junit.Assert.*; -import static org.openmhealth.schema.pojos.generic.MassUnitValue.MassUnit.lb; - -/** - * Ensures that the body weight builder class - * generates JSON that conforms to the body weight clinical schema. - * - * @author Danilo Bonilla - */ -public class BodyWeightBuilderTest { - - @Test - public void test() throws IOException, ProcessingException { - - final String BODY_WEIGHT_SCHEMA = "http://www.openmhealth.org/schema/omh/clinical/body-weight-1.0.json"; - - ObjectMapper mapper = new ObjectMapper(); - - final JsonSchemaFactory factory = JsonSchemaFactory.byDefault(); - final JsonSchema schema = factory.getJsonSchema(BODY_WEIGHT_SCHEMA); - - ProcessingReport report; - - BodyWeightBuilder builder = new BodyWeightBuilder(); - - BodyWeight invalidBodyWeight = builder.build(); - String invalidJson = mapper.writeValueAsString(invalidBodyWeight); - - report = schema.validate(mapper.readTree(invalidJson)); - System.out.println(report); - assertFalse("Expected invalid result but got success", report.isSuccess()); - - builder.setWeight(230d, lb); - builder.setTimeTaken(new DateTime()); - BodyWeight bodyWeight = builder.build(); - - assertNotNull(bodyWeight.getEffectiveTimeFrame()); - assertNotNull(bodyWeight.getMassUnitValue()); - assertEquals(bodyWeight.getMassUnitValue().getUnit(),lb); - assertEquals(bodyWeight.getMassUnitValue().getValue(),new BigDecimal(230d)); - - String rawJson = mapper.writeValueAsString(bodyWeight); - - report = schema.validate(mapper.readTree(rawJson)); - System.out.println(report); - - assertTrue("Expected valid result!", report.isSuccess()); - } -} diff --git a/java-schema-objects/src/test/java/org/openmhealth/schema/pojos/build/HeartRateBuilderTest.java b/java-schema-objects/src/test/java/org/openmhealth/schema/pojos/build/HeartRateBuilderTest.java deleted file mode 100644 index 4359a935..00000000 --- a/java-schema-objects/src/test/java/org/openmhealth/schema/pojos/build/HeartRateBuilderTest.java +++ /dev/null @@ -1,77 +0,0 @@ -/* - * Copyright 2014 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.schema.pojos.build; - -import com.fasterxml.jackson.databind.ObjectMapper; -import com.github.fge.jsonschema.core.exceptions.ProcessingException; -import com.github.fge.jsonschema.core.report.ProcessingReport; -import com.github.fge.jsonschema.main.JsonSchema; -import com.github.fge.jsonschema.main.JsonSchemaFactory; -import org.joda.time.DateTime; -import org.junit.Test; -import org.openmhealth.schema.pojos.HeartRate; -import org.openmhealth.schema.pojos.TemporalRelationshipToPhysicalActivity; - -import java.io.IOException; - -import static org.junit.Assert.*; - -/** - * @author Danilo Bonilla - */ -public class HeartRateBuilderTest { - - @Test - public void test() throws IOException, ProcessingException { - - final String HEART_RATE_SCHEMA = "http://www.openmhealth.org/schema/omh/clinical/heart-rate-1.0.json"; - - ObjectMapper mapper = new ObjectMapper(); - - final JsonSchemaFactory factory = JsonSchemaFactory.byDefault(); - final JsonSchema schema = factory.getJsonSchema(HEART_RATE_SCHEMA); - - ProcessingReport report; - - HeartRateBuilder builder = new HeartRateBuilder(); - - HeartRate invalidHeartRate = builder.build(); - String invalidJson = mapper.writeValueAsString(invalidHeartRate); - - report = schema.validate(mapper.readTree(invalidJson)); - System.out.println(report); - assertFalse("Expected invalid result but got success", report.isSuccess()); - - builder.withRate(234); - builder.withTimeTaken(new DateTime()); - builder.withTimeTakenDescription( - TemporalRelationshipToPhysicalActivity.after_exercise); - - HeartRate heartRate = builder.build(); - - assertNotNull(heartRate.getEffectiveTimeFrame()); - assertNotNull(heartRate.getHeartRate()); - - String rawJson = mapper.writeValueAsString(heartRate); - - report = schema.validate(mapper.readTree(rawJson)); - System.out.println(report); - - assertTrue("Expected valid result!", report.isSuccess()); - } - -} diff --git a/java-schema-objects/src/test/java/org/openmhealth/schema/pojos/build/SleepBuilderTest.java b/java-schema-objects/src/test/java/org/openmhealth/schema/pojos/build/SleepBuilderTest.java deleted file mode 100644 index 7d97fe6f..00000000 --- a/java-schema-objects/src/test/java/org/openmhealth/schema/pojos/build/SleepBuilderTest.java +++ /dev/null @@ -1,85 +0,0 @@ -/* - * Copyright 2014 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.schema.pojos.build; - - -import com.fasterxml.jackson.databind.ObjectMapper; -import com.github.fge.jsonschema.core.exceptions.ProcessingException; -import com.github.fge.jsonschema.core.report.ProcessingReport; -import com.github.fge.jsonschema.main.JsonSchema; -import com.github.fge.jsonschema.main.JsonSchemaFactory; -import org.joda.time.DateTime; -import org.junit.Test; -import org.openmhealth.schema.pojos.SleepDuration; - -import java.io.IOException; - -import static org.junit.Assert.*; -import static org.openmhealth.schema.pojos.SleepDurationUnitValue.Unit.min; - -/** - * @author Danilo Bonilla - */ -public class SleepBuilderTest { - - @Test -// @Ignore("requires updates to external schemas to pass") - public void test() throws IOException, ProcessingException { - - final String SLEEP_DURATION_SCHEMA = "http://www.openmhealth.org/schema/omh/clinical/sleep-duration-1.0.json"; - - ObjectMapper mapper = new ObjectMapper(); - - final JsonSchemaFactory factory = JsonSchemaFactory.byDefault(); - final JsonSchema schema = factory.getJsonSchema(SLEEP_DURATION_SCHEMA); - - ProcessingReport report; - - SleepDurationBuilder builder = new SleepDurationBuilder(); - - SleepDuration invalidSleepDuration = builder.build(); - String invalidJson = mapper.writeValueAsString(invalidSleepDuration); - - report = schema.validate(mapper.readTree(invalidJson)); - System.out.println(report); - assertFalse("Expected invalid result but got success", report.isSuccess()); - - SleepDuration sleepDuration = new SleepDurationBuilder() - .withStartAndEndAndDuration( - new DateTime(), new DateTime().plusMinutes(250), 3d, min) - .setNotes("Tossed and turned") - .build(); - - DateTime timeStamp = sleepDuration.getTimeStamp(); - assertNotNull(timeStamp); - - String rawJson = mapper.writeValueAsString(sleepDuration); - - SleepDuration deserialized = mapper.readValue(rawJson, SleepDuration.class); - - assertNotNull(deserialized.getEffectiveTimeFrame().getTimeInterval().getStartTime()); - assertNotNull(deserialized.getEffectiveTimeFrame().getTimeInterval().getEndTime()); - assertNotNull(deserialized.getSleepDurationUnitValue().getUnit()); - assertNotNull(deserialized.getSleepDurationUnitValue().getValue()); - - report = schema.validate(mapper.readTree(rawJson)); - System.out.println(report); - - assertTrue("Expected valid result!", report.isSuccess()); - } - -} diff --git a/java-schema-objects/src/test/java/org/openmhealth/schema/pojos/build/StepBuilderTest.java b/java-schema-objects/src/test/java/org/openmhealth/schema/pojos/build/StepBuilderTest.java deleted file mode 100644 index faffdca7..00000000 --- a/java-schema-objects/src/test/java/org/openmhealth/schema/pojos/build/StepBuilderTest.java +++ /dev/null @@ -1,83 +0,0 @@ -/* - * Copyright 2014 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.schema.pojos.build; - -import com.fasterxml.jackson.databind.ObjectMapper; -import com.github.fge.jsonschema.core.exceptions.ProcessingException; -import com.github.fge.jsonschema.core.report.ProcessingReport; -import com.github.fge.jsonschema.main.JsonSchema; -import com.github.fge.jsonschema.main.JsonSchemaFactory; -import org.joda.time.DateTime; -import org.junit.Test; -import org.openmhealth.schema.pojos.StepCount; - -import java.io.IOException; - -import static org.junit.Assert.*; -import static org.openmhealth.schema.pojos.generic.DurationUnitValue.DurationUnit.sec; - -/** - * @author Danilo Bonilla - */ -public class StepBuilderTest { - - @Test - //@Ignore("requires updates to external schemas to pass") - public void testParse() throws IOException, ProcessingException { - - final String STEP_COUNT_SCHEMA = "http://www.openmhealth.org/schema/omh/clinical/step-count-1.0.json"; - - ObjectMapper mapper = new ObjectMapper(); - - final JsonSchemaFactory factory = JsonSchemaFactory.byDefault(); - final JsonSchema schema = factory.getJsonSchema(STEP_COUNT_SCHEMA); - - ProcessingReport report; - - StepCountBuilder builder = new StepCountBuilder(); - - StepCount invalidStepCount = builder.build(); - String invalidJson = mapper.writeValueAsString(invalidStepCount); - - report = schema.validate(mapper.readTree(invalidJson)); - System.out.println(report); - assertFalse("Expected invalid result but got success", report.isSuccess()); - - StepCount stepCount = new StepCountBuilder() - .withStartAndDuration(new DateTime(), 3d, sec) - .setSteps(345) - .build(); - - DateTime timeStamp = stepCount.getTimeStamp(); - assertNotNull(timeStamp); - - String rawJson = mapper.writeValueAsString(stepCount); - - StepCount deserialized = mapper.readValue(rawJson, StepCount.class); - - assertNotNull(deserialized.getEffectiveTimeFrame().getTimeInterval().getStartTime()); - assertNotNull(deserialized.getEffectiveTimeFrame().getTimeInterval().getDuration().getUnit()); - assertNotNull(deserialized.getEffectiveTimeFrame().getTimeInterval().getDuration().getValue()); - assertNotNull(deserialized.getStepCount()); - - report = schema.validate(mapper.readTree(rawJson)); - System.out.println(report); - - assertTrue("Expected valid result!", report.isSuccess()); - } - -} diff --git a/java-shim-sdk/build.gradle b/java-shim-sdk/build.gradle index 2ac687de..433b2a57 100644 --- a/java-shim-sdk/build.gradle +++ b/java-shim-sdk/build.gradle @@ -1,8 +1,97 @@ +/* + * 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. + */ + +buildscript { + repositories { + jcenter() + } + dependencies { + classpath 'com.jfrog.bintray.gradle:gradle-bintray-plugin:1.2' + } +} + apply plugin: 'maven' +apply plugin: 'maven-publish' +apply plugin: 'com.jfrog.bintray' // see https://github.com/bintray/gradle-bintray-plugin for details + +archivesBaseName = 'omh-shim-sdk' +version = '1.0.0' -version = shimServerVersion +ext { + jacksonVersion = '2.5.3' +} dependencies { - compile project(':java-schema-objects') - compile "joda-time:joda-time:2.4" + compile "com.google.guava:guava:18.0" + compile "com.fasterxml.jackson.core:jackson-annotations:${jacksonVersion}" + compile "com.fasterxml.jackson.core:jackson-databind:${jacksonVersion}" + compile "joda-time:joda-time:2.5" // TODO remove this when refactored to java.time + compile "org.openmhealth.schema:omh-schema-sdk:${omhSchemaSdkVersion}" + compile 'org.slf4j:slf4j-api:1.7.12' + + testCompile 'org.testng:testng:6.8.21' + testCompile 'org.hamcrest:hamcrest-library:1.3' + + testRuntime 'ch.qos.logback:logback-classic:1.1.3' +} + +task sourcesJar(type: Jar, dependsOn: classes) { + classifier = 'sources' + from sourceSets.main.allSource +} + +task javadocJar(type: Jar, dependsOn: Javadoc) { + classifier = 'javadoc' + from javadoc.destinationDir +} + +publishing { + publications { + jar(MavenPublication) { + artifactId 'omh-shim-sdk' + from components.java + } + sources(MavenPublication) { + artifactId 'omh-shim-sdk' + artifact sourcesJar + } + javadoc(MavenPublication) { + artifactId 'omh-shim-sdk' + artifact javadocJar + } + } +} + +bintray { + user = project.hasProperty('bintray_user') ? bintray_user : '' + key = project.hasProperty('bintray_api_key') ? bintray_api_key : '' + + publications = ['jar', 'sources', 'javadoc'] + dryRun = false + publish = false + pkg { + repo = 'maven' + name = 'omh-shim-sdk' + userOrg = 'openmhealth' + desc = 'A library containing Java classes that help you build Open mHealth shims.' + websiteUrl = 'https://github.com/openmhealth/shimmer' + issueTrackerUrl = 'https://github.com/openmhealth/shimmer/issues' + vcsUrl = 'https://github.com/openmhealth/shimmer.git' + licenses = ['Apache-2.0'] + labels = ['shims'] + publicDownloadNumbers = true + } } diff --git a/java-shim-sdk/src/main/java/org/openmhealth/shim/AccessParameters.java b/java-shim-sdk/src/main/java/org/openmhealth/shim/AccessParameters.java index 1d88d00a..f81db4dc 100644 --- a/java-shim-sdk/src/main/java/org/openmhealth/shim/AccessParameters.java +++ b/java-shim-sdk/src/main/java/org/openmhealth/shim/AccessParameters.java @@ -17,8 +17,7 @@ package org.openmhealth.shim; -import org.joda.time.DateTime; - +import java.time.LocalDateTime; import java.util.LinkedHashMap; import java.util.Map; @@ -45,7 +44,7 @@ public class AccessParameters { private String stateKey; - private DateTime dateCreated = new DateTime(); + private LocalDateTime dateCreated = LocalDateTime.now(); private byte[] serializedToken; //Required only by spring oauth2 @@ -65,11 +64,11 @@ public void setShimKey(String shimKey) { this.shimKey = shimKey; } - public DateTime getDateCreated() { + public LocalDateTime getDateCreated() { return dateCreated; } - public void setDateCreated(DateTime dateCreated) { + public void setDateCreated(LocalDateTime dateCreated) { this.dateCreated = dateCreated; } diff --git a/java-shim-sdk/src/main/java/org/openmhealth/shim/ShimDataRequest.java b/java-shim-sdk/src/main/java/org/openmhealth/shim/ShimDataRequest.java index d8768484..45c16873 100644 --- a/java-shim-sdk/src/main/java/org/openmhealth/shim/ShimDataRequest.java +++ b/java-shim-sdk/src/main/java/org/openmhealth/shim/ShimDataRequest.java @@ -18,6 +18,7 @@ import org.joda.time.DateTime; +import java.time.OffsetDateTime; import java.util.List; /** @@ -40,15 +41,15 @@ public class ShimDataRequest { private AccessParameters accessParameters; /** + * // TODO replace this with filters on effective time, using the Data Point API * The start date for the data being retrieved */ - - private DateTime startDate; + private OffsetDateTime startDateTime; /** * The end date for the data being retrieved */ - private DateTime endDate; + private OffsetDateTime endDateTime; /** * List of columns required @@ -70,23 +71,23 @@ public class ShimDataRequest { * from the external data provider, otherwise * returns raw data. */ - private boolean normalize = false; + private boolean normalize = true; public ShimDataRequest() { } public ShimDataRequest(String dataTypeKey, AccessParameters accessParameters, - DateTime startDate, - DateTime endDate, + OffsetDateTime startDateTime, + OffsetDateTime endDateTime, List columnList, Long numToSkip, Long numToReturn, boolean normalize) { this.dataTypeKey = dataTypeKey; this.accessParameters = accessParameters; - this.startDate = startDate; - this.endDate = endDate; + this.startDateTime = startDateTime; + this.endDateTime = endDateTime; this.columnList = columnList; this.numToSkip = numToSkip; this.numToReturn = numToReturn; @@ -101,12 +102,28 @@ public void setAccessParameters(AccessParameters accessParameters) { this.accessParameters = accessParameters; } - public void setStartDate(DateTime startDate) { - this.startDate = startDate; + public OffsetDateTime getStartDateTime() { + return startDateTime; } - public void setEndDate(DateTime endDate) { - this.endDate = endDate; + public DateTime getStartDate() { + return getStartDateTime() == null ? null : new DateTime(getStartDateTime().toInstant()); + } + + public void setStartDateTime(OffsetDateTime startDateTime) { + this.startDateTime = startDateTime; + } + + public OffsetDateTime getEndDateTime() { + return endDateTime; + } + + public DateTime getEndDate() { + return getStartDateTime() == null ? null : new DateTime(getStartDateTime().toInstant()); + } + + public void setEndDateTime(OffsetDateTime endDateTime) { + this.endDateTime = endDateTime; } public void setColumnList(List columnList) { @@ -129,14 +146,6 @@ public AccessParameters getAccessParameters() { return accessParameters; } - public DateTime getStartDate() { - return startDate; - } - - public DateTime getEndDate() { - return endDate; - } - public List getColumnList() { return columnList; } diff --git a/java-shim-sdk/src/main/java/org/openmhealth/shim/ShimDataResponse.java b/java-shim-sdk/src/main/java/org/openmhealth/shim/ShimDataResponse.java index 13455d34..1bf16034 100644 --- a/java-shim-sdk/src/main/java/org/openmhealth/shim/ShimDataResponse.java +++ b/java-shim-sdk/src/main/java/org/openmhealth/shim/ShimDataResponse.java @@ -22,7 +22,7 @@ * Wrapper for responses received from shims. *

* todo: expand to include original parameters - * + * TODO there's no pagination information, how does a caller know how to proceed? * @author Danilo Bonilla */ public class ShimDataResponse { @@ -41,6 +41,7 @@ public void setShim(String shim) { this.shim = shim; } + // TODO in seconds since the epoch? needs to be documented public Long getTimeStamp() { return timeStamp; } @@ -61,6 +62,7 @@ public static ShimDataResponse empty(String shimKey) { ShimDataResponse response = new ShimDataResponse(); response.setShim(shimKey); response.setBody(null); + response.setTimeStamp( Calendar.getInstance().getTimeInMillis() / 1000); return response; diff --git a/java-shim-sdk/src/main/java/org/openmhealth/shim/common/mapper/DataPointMapper.java b/java-shim-sdk/src/main/java/org/openmhealth/shim/common/mapper/DataPointMapper.java new file mode 100644 index 00000000..cdc61f3b --- /dev/null +++ b/java-shim-sdk/src/main/java/org/openmhealth/shim/common/mapper/DataPointMapper.java @@ -0,0 +1,27 @@ +package org.openmhealth.shim.common.mapper; + + +import org.openmhealth.schema.domain.omh.DataPoint; + +import java.util.List; + + +/** + * A mapper that creates data points from one or more inputs. + * + * @param the body type of the data points to create + * @param the input type + * @author Emerson Farrugia + */ +public interface DataPointMapper { + + /** + * Maps one or more inputs into one or more data points. This cardinality allows a mapper to use different inputs + * to assemble a data point, e.g. combining a user profile API response and a blood pressure API response to build + * an identified blood pressure data point. + * + * @param inputs the list of inputs + * @return the list of data points + */ + List> asDataPoints(List inputs); +} diff --git a/java-shim-sdk/src/main/java/org/openmhealth/shim/common/mapper/IncompatibleJsonNodeMappingException.java b/java-shim-sdk/src/main/java/org/openmhealth/shim/common/mapper/IncompatibleJsonNodeMappingException.java new file mode 100644 index 00000000..deaa93bb --- /dev/null +++ b/java-shim-sdk/src/main/java/org/openmhealth/shim/common/mapper/IncompatibleJsonNodeMappingException.java @@ -0,0 +1,49 @@ +package org.openmhealth.shim.common.mapper; + +import com.fasterxml.jackson.databind.JsonNode; + +import static java.lang.String.format; + + +/** + * An exception thrown to indicate that the value of a {@link JsonNode} doesn't match an expected type. + * + * @author Emerson Farrugia + */ +public class IncompatibleJsonNodeMappingException extends JsonNodeMappingException { + + private Class expectedType; + + /** + * @param expectedType the expected type of the node value + */ + public IncompatibleJsonNodeMappingException(JsonNode parentNode, String path, Class expectedType) { + super(parentNode, path); + + this.expectedType = expectedType; + } + + /** + * @param expectedType the expected type of the node value + * @param cause the cause + */ + public IncompatibleJsonNodeMappingException(JsonNode parentNode, String path, Class expectedType, + Throwable cause) { + + super(parentNode, path, cause); + + this.expectedType = expectedType; + } + + public Class getExpectedType() { + return expectedType; + } + + @Override + public String getMessage() { + String value = getParentNode().get(getPath()).asText(); + + return format("The field '%s' with value '%s' doesn't have expected type '%s' in node '%s'.", + getPath(), value, getExpectedType(), getParentNode()); + } +} diff --git a/java-shim-sdk/src/main/java/org/openmhealth/shim/common/mapper/JsonNodeDataPointMapper.java b/java-shim-sdk/src/main/java/org/openmhealth/shim/common/mapper/JsonNodeDataPointMapper.java new file mode 100644 index 00000000..15d24e36 --- /dev/null +++ b/java-shim-sdk/src/main/java/org/openmhealth/shim/common/mapper/JsonNodeDataPointMapper.java @@ -0,0 +1,14 @@ +package org.openmhealth.shim.common.mapper; + +import com.fasterxml.jackson.databind.JsonNode; + + +/** + * A mapper that creates data points from one or more {@link JsonNode} inputs. + * + * @param the body type of the data points to create + * @author Emerson Farrugia + */ +public interface JsonNodeDataPointMapper extends DataPointMapper { + +} diff --git a/java-shim-sdk/src/main/java/org/openmhealth/shim/common/mapper/JsonNodeMappingException.java b/java-shim-sdk/src/main/java/org/openmhealth/shim/common/mapper/JsonNodeMappingException.java new file mode 100644 index 00000000..2f165fe4 --- /dev/null +++ b/java-shim-sdk/src/main/java/org/openmhealth/shim/common/mapper/JsonNodeMappingException.java @@ -0,0 +1,61 @@ +package org.openmhealth.shim.common.mapper; + +import com.fasterxml.jackson.databind.JsonNode; + + +/** + * An exception thrown to indicate a problem when mapping a {@link JsonNode}. + * + * @author Emerson Farrugia + */ +public class JsonNodeMappingException extends RuntimeException { + + private JsonNode parentNode; + private String path; + + + /** + * @param message the detail message + */ + public JsonNodeMappingException(String message) { + super(message); + } + + /** + * @param message the detail message + * @param cause the cause + */ + public JsonNodeMappingException(String message, Throwable cause) { + super(message, cause); + } + + /** + * @param parentNode the parent of the node in question + * @param path the path to the node + */ + public JsonNodeMappingException(JsonNode parentNode, String path) { + + this.parentNode = parentNode; + this.path = path; + } + + /** + * @param parentNode the parent of the node in question + * @param path the path to the node + * @param cause the cause + */ + public JsonNodeMappingException(JsonNode parentNode, String path, Throwable cause) { + super(cause); + + this.parentNode = parentNode; + this.path = path; + } + + public JsonNode getParentNode() { + return parentNode; + } + + public String getPath() { + return path; + } +} diff --git a/java-shim-sdk/src/main/java/org/openmhealth/shim/common/mapper/JsonNodeMappingSupport.java b/java-shim-sdk/src/main/java/org/openmhealth/shim/common/mapper/JsonNodeMappingSupport.java new file mode 100644 index 00000000..c283df19 --- /dev/null +++ b/java-shim-sdk/src/main/java/org/openmhealth/shim/common/mapper/JsonNodeMappingSupport.java @@ -0,0 +1,426 @@ +package org.openmhealth.shim.common.mapper; + +import com.fasterxml.jackson.databind.JsonNode; +import com.google.common.base.Splitter; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.time.*; +import java.time.format.DateTimeFormatter; +import java.time.format.DateTimeParseException; +import java.util.Optional; +import java.util.function.Function; + +import static java.time.format.DateTimeFormatter.ISO_LOCAL_DATE_TIME; +import static java.util.Optional.empty; + + +/** + * A set of utility methods to help with mapping {@link JsonNode} objects. + * + * @author Emerson Farrugia + */ +public class JsonNodeMappingSupport { + + private static final Logger logger = LoggerFactory.getLogger(JsonNodeMappingSupport.class); + + + /** + * @param parentNode a parent node + * @param path a path to a child node + * @return the child node reached by traversing the path, where dots denote nested nodes + * @throws MissingJsonNodeMappingException if the child node doesn't exist + */ + public static JsonNode asRequiredNode(JsonNode parentNode, String path) { + + Iterable pathSegments = Splitter.on(".").split(path); + JsonNode node = parentNode; + + for (String pathSegment : pathSegments) { + + if (!node.hasNonNull(pathSegment)) { + throw new MissingJsonNodeMappingException(node, pathSegment); + } + + node = node.path(pathSegment); + } + + return node; + } + + /** + * @param parentNode a parent node + * @param path the path to a child node + * @param typeChecker the function to check if the type is compatible + * @param converter the function to convert the node to a value + * @param the type of the value to convert to + * @return the value of the child node + * @throws MissingJsonNodeMappingException if the child doesn't exist + * @throws IncompatibleJsonNodeMappingException if the value of the child node isn't compatible + */ + public static T asRequiredValue(JsonNode parentNode, String path, Function typeChecker, + Function converter, Class targetType) { + + JsonNode childNode = asRequiredNode(parentNode, path); + + if (!typeChecker.apply(childNode)) { + throw new IncompatibleJsonNodeMappingException(parentNode, path, targetType); + } + + return converter.apply(childNode); + } + + /** + * @param parentNode a parent node + * @param path the path to a child node + * @return the value of the child node as a string + * @throws MissingJsonNodeMappingException if the child doesn't exist + * @throws IncompatibleJsonNodeMappingException if the value of the child node isn't textual + */ + public static String asRequiredString(JsonNode parentNode, String path) { + + return asRequiredValue(parentNode, path, JsonNode::isTextual, JsonNode::textValue, String.class); + } + + // TODO add tests + /** + * @param parentNode a parent node + * @param path the path to a child node + * @return the value of the child node as a boolean + * @throws MissingJsonNodeMappingException if the child doesn't exist + * @throws IncompatibleJsonNodeMappingException if the value of the child node isn't a boolean + */ + public static Boolean asRequiredBoolean(JsonNode parentNode, String path) { + + return asRequiredValue(parentNode, path, JsonNode::isBoolean, JsonNode::booleanValue, Boolean.class); + } + + /** + * @param parentNode a parent node + * @param path the path to a child node + * @return the value of the child node as a long + * @throws MissingJsonNodeMappingException if the child doesn't exist + * @throws IncompatibleJsonNodeMappingException if the value of the child node isn't an integer + */ + public static Long asRequiredLong(JsonNode parentNode, String path) { + + return asRequiredValue(parentNode, path, JsonNode::isIntegralNumber, JsonNode::longValue, Long.class); + } + + // TODO add tests + /** + * @param parentNode a parent node + * @param path the path to a child node + * @return the value of the child node as an integer + * @throws MissingJsonNodeMappingException if the child doesn't exist + * @throws IncompatibleJsonNodeMappingException if the value of the child node isn't an integer + */ + public static Integer asRequiredInteger(JsonNode parentNode, String path) { + + return asRequiredValue(parentNode, path, JsonNode::isIntegralNumber, JsonNode::intValue, Integer.class); + } + + /** + * @param parentNode a parent node + * @param path the path to a child node + * @return the value of the child node as a double + * @throws MissingJsonNodeMappingException if the child doesn't exist + * @throws IncompatibleJsonNodeMappingException if the value of the child node isn't numeric + */ + public static Double asRequiredDouble(JsonNode parentNode, String path) { + + return asRequiredValue(parentNode, path, JsonNode::isNumber, JsonNode::doubleValue, Double.class); + } + + /** + * @param parentNode a parent node + * @param path the path to a child node + * @return the value of the child node as a {@link LocalDate} + * @throws MissingJsonNodeMappingException if the child doesn't exist + * @throws IncompatibleJsonNodeMappingException if the value of the child node isn't a date + */ + // TODO overload with a DateTimeFormatter parameter + public static LocalDate asRequiredLocalDate(JsonNode parentNode, String path) { + + String string = asRequiredString(parentNode, path); + + try { + return LocalDate.parse(string); + } + catch (DateTimeParseException e) { + throw new IncompatibleJsonNodeMappingException(parentNode, path, LocalDate.class, e); + } + } + + /** + * @param parentNode a parent node + * @param path the path to a child node + * @return the value of the child node as an {@link OffsetDateTime} + * @throws MissingJsonNodeMappingException if the child doesn't exist + * @throws IncompatibleJsonNodeMappingException if the value of the child node isn't a date time + */ + public static OffsetDateTime asRequiredOffsetDateTime(JsonNode parentNode, String path) { + + String string = asRequiredString(parentNode, path); + + try { + return OffsetDateTime.parse(string); + } + catch (DateTimeParseException e) { + throw new IncompatibleJsonNodeMappingException(parentNode, path, OffsetDateTime.class, e); + } + } + + /** + * @param parentNode a parent node + * @param path a path to a child node, where dots denote nested nodes + * @return the child node reached by traversing the path, or an empty optional if the child doesn't exist + */ + public static Optional asOptionalNode(final JsonNode parentNode, final String path) { + + Iterable pathSegments = Splitter.on(".").split(path); + JsonNode node = parentNode; + + for (String pathSegment : pathSegments) { + JsonNode childNode = node.path(pathSegment); + + if (childNode.isMissingNode()) { + logger.debug("A '{}' field wasn't found in node '{}'.", pathSegment, node); + return empty(); + } + + if (childNode.isNull()) { + logger.debug("The '{}' field is null in node '{}'.", pathSegment, node); + return empty(); + } + + node = childNode; + } + + return Optional.of(node); + } + + /** + * @param parentNode a parent node + * @param path the path to a child node + * @param typeChecker the function to check if the type is compatible + * @param converter the function to convert the node to a value + * @param the type of the value to convert to + * @return the value of the child node, or an empty optional if the child doesn't exist or if the + * value of the child node isn't compatible + */ + public static Optional asOptionalValue(JsonNode parentNode, String path, + Function typeChecker, Function converter) { + + JsonNode childNode = asOptionalNode(parentNode, path).orElse(null); + + if (childNode == null) { + return empty(); + } + + if (!typeChecker.apply(childNode)) { + logger.warn("The '{}' field in node '{}' isn't compatible.", path, parentNode); + return empty(); + } + + return Optional.of(converter.apply(childNode)); + } + + /** + * @param parentNode a parent node + * @param path the path to a child node + * @return the value of the child node as a string, or an empty optional if the child doesn't exist or if the + * value of the child node isn't textual + */ + public static Optional asOptionalString(JsonNode parentNode, String path) { + + return asOptionalValue(parentNode, path, JsonNode::isTextual, JsonNode::textValue); + } + + /** + * @param parentNode a parent node + * @param path the path to a child node + * @return the value of the child node as a boolean, or an empty optional if the child doesn't exist or if the + * value of the child node isn't boolean + */ + public static Optional asOptionalBoolean(JsonNode parentNode, String path) { + + return asOptionalValue(parentNode, path, JsonNode::isBoolean, JsonNode::booleanValue); + } + + /** + * @param parentNode a parent node + * @param path the path to a child node + * @return the value of the child node as a date time, or an empty optional if the child doesn't exist or if the + * value of the child node isn't a date time + */ + public static Optional asOptionalOffsetDateTime(JsonNode parentNode, String path) { + + Optional string = asOptionalString(parentNode, path); + + if (!string.isPresent()) { + return empty(); + } + + OffsetDateTime dateTime = null; + + try { + dateTime = OffsetDateTime.parse(string.get()); + } + catch (DateTimeParseException e) { + logger.warn("The '{}' field in node '{}' with value '{}' isn't a valid timestamp.", + path, parentNode, string.get(), e); + } + + return Optional.ofNullable(dateTime); + } + + /** + * @param parentNode a parent node + * @param path the path to a child node + * @param formatter the formatter to use to parse the value of the child node + * @return the value of the child node as a date time, or an empty optional if the child doesn't exist or if the + * value of the child node isn't a date time + */ + public static Optional asOptionalLocalDateTime(JsonNode parentNode, String path, + DateTimeFormatter formatter) { + + Optional string = asOptionalString(parentNode, path); + + if (!string.isPresent()) { + return empty(); + } + + LocalDateTime dateTime = null; + + try { + dateTime = LocalDateTime.parse(string.get(), formatter); + } + catch (DateTimeParseException e) { + logger.warn("The '{}' field in node '{}' with value '{}' isn't a valid timestamp.", + path, parentNode, string.get(), e); + } + + return Optional.ofNullable(dateTime); + } + + /** + * @param parentNode a parent node + * @param path the path to a child node + * @return the value of the child node as a date time, or an empty optional if the child doesn't exist or if the + * value of the child node isn't a date time matching {@link DateTimeFormatter#ISO_LOCAL_DATE_TIME} + */ + public static Optional asOptionalLocalDateTime(JsonNode parentNode, String path) { + + return asOptionalLocalDateTime(parentNode, path, ISO_LOCAL_DATE_TIME); + } + + // TODO refactor this by delegating to existing methods, then add tests + public static Optional asOptionalLocalDateTime(JsonNode parentNode, String pathToDate, + String pathToTime) { + Optional time = asOptionalString(parentNode, pathToTime); + Optional date = asOptionalString(parentNode, pathToDate); + if (!time.isPresent() || !date.isPresent()) { + return empty(); + } + LocalDateTime dateTime = null; + try { + dateTime = LocalDateTime.parse(date.get() + "T" + time.get(), ISO_LOCAL_DATE_TIME); + } + catch (DateTimeParseException e) { + logger.warn( + "The '{}' and '{}' fields in node '{}' with values '{}' and '{}' do not make-up a valid timestamp.", + pathToDate, pathToTime, parentNode, date.get(), time.get(), e); + } + return Optional.ofNullable(dateTime); + } + + // TODO add Javadoc and tests + public static Optional asOptionalLocalDate(JsonNode parentNode, String path) { + + return asOptionalLocalDate(parentNode, path, DateTimeFormatter.ISO_LOCAL_DATE); + } + + // TODO add Javadoc and tests + public static Optional asOptionalLocalDate(JsonNode parentNode, String path, + DateTimeFormatter dateFormat) { + + Optional string = asOptionalString(parentNode, path); + + if (!string.isPresent()) { + return empty(); + } + + LocalDate localDate = null; + + try { + localDate = LocalDate.parse(string.get(), dateFormat); + } + catch (DateTimeParseException e) { + logger.warn("The '{}' field in node '{}' with value '{}' isn't a valid date.", + path, parentNode, string.get(), e); + } + + return Optional.ofNullable(localDate); + } + + /** + * @param parentNode a parent node + * @param path the path to a child node + * @return the value of the child node as a double, or an empty optional if the child doesn't exist or if the + * value of the child node isn't numeric + */ + public static Optional asOptionalDouble(JsonNode parentNode, String path) { + + return asOptionalValue(parentNode, path, JsonNode::isNumber, JsonNode::doubleValue); + } + + /** + * @param parentNode a parent node + * @param path the path to a child node + * @return the value of the child node as a long, or an empty optional if the child doesn't exist or if the + * value of the child node isn't an integer + */ + public static Optional asOptionalLong(JsonNode parentNode, String path) { + + return asOptionalValue(parentNode, path, JsonNode::isIntegralNumber, JsonNode::longValue); + } + + /** + * @param parentNode a parent node + * @param path the path to a child node + * @return the value of the child node as an integer, or an empty optional if the child doesn't exist or if the + * value of the child node isn't an integer + */ + public static Optional asOptionalInteger(JsonNode parentNode, String path) { + + return asOptionalValue(parentNode, path, JsonNode::isIntegralNumber, JsonNode::intValue); + } + + /** + * @param parentNode a parent node + * @param path the path to a child node + * @return the value of the child node as a {@link ZoneId}, or an empty optional if the child doesn't exist or if + * the value of the child node isn't a valid time zone + */ + public static Optional asOptionalZoneId(JsonNode parentNode, String path) { + + Optional string = asOptionalString(parentNode, path); + + if (!string.isPresent()) { + return empty(); + } + + ZoneId zoneId = null; + + try { + zoneId = ZoneId.of(string.get()); + } + catch (DateTimeException e) { + logger.warn("The '{}' field in node '{}' with value '{}' isn't a valid time zone.", + path, parentNode, string.get(), e); + } + + return Optional.ofNullable(zoneId); + } +} diff --git a/java-shim-sdk/src/main/java/org/openmhealth/shim/common/mapper/MissingJsonNodeMappingException.java b/java-shim-sdk/src/main/java/org/openmhealth/shim/common/mapper/MissingJsonNodeMappingException.java new file mode 100644 index 00000000..54557c6d --- /dev/null +++ b/java-shim-sdk/src/main/java/org/openmhealth/shim/common/mapper/MissingJsonNodeMappingException.java @@ -0,0 +1,21 @@ +package org.openmhealth.shim.common.mapper; + +import com.fasterxml.jackson.databind.JsonNode; + + +/** + * An exception thrown to indicate that a required {@link JsonNode} is missing. + * + * @author Emerson Farrugia + */ +public class MissingJsonNodeMappingException extends JsonNodeMappingException { + + public MissingJsonNodeMappingException(JsonNode parentNode, String path) { + super(parentNode, path); + } + + @Override + public String getMessage() { + return String.format("The required field '%s' wasn't found in node '%s'.", getPath(), getParentNode()); + } +} diff --git a/java-shim-sdk/src/test/java/org/openmhealth/shim/common/mapper/JsonNodeMappingSupportUnitTests.java b/java-shim-sdk/src/test/java/org/openmhealth/shim/common/mapper/JsonNodeMappingSupportUnitTests.java new file mode 100644 index 00000000..189bff86 --- /dev/null +++ b/java-shim-sdk/src/test/java/org/openmhealth/shim/common/mapper/JsonNodeMappingSupportUnitTests.java @@ -0,0 +1,611 @@ +package org.openmhealth.shim.common.mapper; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import org.testng.annotations.BeforeClass; +import org.testng.annotations.Test; + +import java.io.IOException; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.OffsetDateTime; +import java.time.ZoneOffset; +import java.time.format.DateTimeFormatter; +import java.util.Optional; + +import static org.hamcrest.CoreMatchers.equalTo; +import static org.hamcrest.CoreMatchers.notNullValue; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.openmhealth.shim.common.mapper.JsonNodeMappingSupport.*; + + +/** + * @author Emerson Farrugia + */ +public class JsonNodeMappingSupportUnitTests { + + private static final ObjectMapper objectMapper = new ObjectMapper(); + private static JsonNode testNode; + + @BeforeClass + public static void initializeTestNode() throws IOException { + + testNode = objectMapper.readTree("{\n" + + " \"number\": 2.3,\n" + + " \"integer\": 2,\n" + + " \"string\": \"hi\",\n" + + " \"boolean\": true,\n" + + " \"empty\": null,\n" + + " \"date_time\": \"2014-01-01T12:15:04+02:00\",\n" + + " \"date\": \"2014-01-01\",\n" + + " \"local_date_time\": \"2014-01-01T12:15:04\",\n" + + " \"custom_local_date_time\": \"Fri, 1 Aug 2014 06:53:05\",\n" + + " \"nested\": {\n" + + " \"empty\": null,\n" + + " \"string\": \"hi\"\n" + + " }\n" + + "}"); + } + + @Test(expectedExceptions = JsonNodeMappingException.class) + public void asRequiredNodeShouldThrowExceptionOnMissingNode() { + + asRequiredNode(testNode, "foo"); + } + + @Test(expectedExceptions = JsonNodeMappingException.class) + public void asRequiredNodeShouldThrowExceptionOnNullNode() { + + asRequiredNode(testNode, "empty"); + } + + @Test + public void asRequiredNodeShouldReturnNodeWhenPresent() { + + JsonNode node = asRequiredNode(testNode, "string"); + + assertThat(node, notNullValue()); + assertThat(node.isMissingNode(), equalTo(false)); + assertThat(node.asText(), equalTo("hi")); + } + + @Test(expectedExceptions = JsonNodeMappingException.class) + public void asRequiredNodeShouldThrowExceptionOnMissingParentNode() { + + asRequiredNode(testNode, "foo.integer"); + } + + @Test(expectedExceptions = JsonNodeMappingException.class) + public void asRequiredNodeShouldThrowExceptionOnMissingChildNode() { + + asRequiredNode(testNode, "nested.foo"); + } + + @Test(expectedExceptions = JsonNodeMappingException.class) + public void asRequiredNodeShouldThrowExceptionOnNestedNullNode() { + + asRequiredNode(testNode, "nested.empty"); + } + + @Test + public void asRequiredNodeShouldReturnNestedNodeWhenPresent() { + + JsonNode node = asRequiredNode(testNode, "nested.string"); + + assertThat(node, notNullValue()); + assertThat(node.isMissingNode(), equalTo(false)); + assertThat(node.asText(), equalTo("hi")); + } + + @Test(expectedExceptions = JsonNodeMappingException.class) + public void asRequiredStringShouldThrowExceptionOnMissingNode() { + + asRequiredString(testNode, "foo"); + } + + @Test(expectedExceptions = JsonNodeMappingException.class) + public void asRequiredStringShouldThrowExceptionOnNullNode() { + + asRequiredString(testNode, "empty"); + } + + @Test(expectedExceptions = JsonNodeMappingException.class) + public void asRequiredStringShouldThrowExceptionOnMismatchedNode() { + + asRequiredString(testNode, "number"); + } + + @Test + public void asRequiredStringShouldReturnStringWhenPresent() { + + String value = asRequiredString(testNode, "string"); + + assertThat(value, notNullValue()); + assertThat(value, equalTo("hi")); + } + + @Test(expectedExceptions = JsonNodeMappingException.class) + public void asRequiredLongShouldThrowExceptionOnMissingNode() { + + asRequiredLong(testNode, "foo"); + } + + @Test(expectedExceptions = JsonNodeMappingException.class) + public void asRequiredLongShouldThrowExceptionOnNullNode() { + + asRequiredLong(testNode, "empty"); + } + + @Test(expectedExceptions = JsonNodeMappingException.class) + public void asRequiredLongShouldThrowExceptionOnMismatchedNode() { + + // since this is floating point + asRequiredLong(testNode, "number"); + } + + @Test + public void asRequiredLongShouldReturnLongWhenPresent() { + + Long value = asRequiredLong(testNode, "integer"); + + assertThat(value, notNullValue()); + assertThat(value, equalTo(2l)); + } + + @Test(expectedExceptions = JsonNodeMappingException.class) + public void asRequiredDoubleShouldThrowExceptionOnMissingNode() { + + asRequiredDouble(testNode, "foo"); + } + + @Test(expectedExceptions = JsonNodeMappingException.class) + public void asRequiredDoubleShouldThrowExceptionOnNullNode() { + + asRequiredDouble(testNode, "empty"); + } + + + @Test(expectedExceptions = JsonNodeMappingException.class) + public void asRequiredDoubleShouldThrowExceptionOnMismatchedNode() { + + asRequiredDouble(testNode, "string"); + } + + @Test + public void asRequiredDoubleShouldReturnDoubleWhenPresent() { + + Double value = asRequiredDouble(testNode, "number"); + + assertThat(value, notNullValue()); + assertThat(value, equalTo(2.3)); + } + + @Test + public void asRequiredDoubleShouldReturnDoubleWhenIntegerIsPresent() { + + Double value = asRequiredDouble(testNode, "integer"); + + assertThat(value, notNullValue()); + assertThat(value, equalTo(2d)); + } + + @Test(expectedExceptions = JsonNodeMappingException.class) + public void asRequiredLocalDateShouldThrowExceptionOnMissingNode() { + + asRequiredLocalDate(testNode, "foo"); + } + + @Test(expectedExceptions = JsonNodeMappingException.class) + public void asRequiredLocalDateShouldThrowExceptionOnNullNode() { + + asRequiredLocalDate(testNode, "empty"); + } + + @Test(expectedExceptions = JsonNodeMappingException.class) + public void asRequiredLocalDateShouldThrowExceptionOnMismatchedNode() { + + asRequiredLocalDate(testNode, "number"); + } + + @Test + public void asRequiredLocalDateShouldReturnLocalDateWhenPresent() { + + LocalDate value = asRequiredLocalDate(testNode, "date"); + + assertThat(value, notNullValue()); + assertThat(value, equalTo(LocalDate.of(2014, 1, 1))); + } + + @Test(expectedExceptions = JsonNodeMappingException.class) + public void asRequiredOffsetDateTimeShouldThrowExceptionOnMissingNode() { + + asRequiredLocalDate(testNode, "foo"); + } + + @Test(expectedExceptions = JsonNodeMappingException.class) + public void asRequiredOffsetDateTimeShouldThrowExceptionOnNullNode() { + + asRequiredLocalDate(testNode, "empty"); + } + + @Test(expectedExceptions = JsonNodeMappingException.class) + public void asRequiredOffsetDateTimeShouldThrowExceptionOnMismatchedNode() { + + asRequiredLocalDate(testNode, "number"); + } + + @Test + public void asRequiredOffsetDateTimeShouldReturnDateTimeWhenPresent() { + + OffsetDateTime value = asRequiredOffsetDateTime(testNode, "date_time"); + + assertThat(value, notNullValue()); + assertThat(value, equalTo(OffsetDateTime.of(2014, 1, 1, 12, 15, 4, 0, ZoneOffset.ofHours(2)))); + } + + @Test + public void asOptionalNodeShouldReturnEmptyOnMissingNode() { + + Optional node = asOptionalNode(testNode, "foo"); + + assertThat(node, notNullValue()); + assertThat(node.isPresent(), equalTo(false)); + } + + @Test + public void asOptionalNodeShouldReturnEmptyOnOnNullNode() { + + Optional node = asOptionalNode(testNode, "empty"); + + assertThat(node, notNullValue()); + assertThat(node.isPresent(), equalTo(false)); + } + + @Test + public void asOptionalNodeShouldReturnNodeWhenPresent() { + + Optional node = asOptionalNode(testNode, "string"); + + assertThat(node, notNullValue()); + assertThat(node.isPresent(), equalTo(true)); + assertThat(node.get().isMissingNode(), equalTo(false)); + assertThat(node.get().asText(), equalTo("hi")); + } + + @Test + public void asOptionalNodeShouldReturnEmptyOnOnMissingParentNode() { + + Optional node = asOptionalNode(testNode, "foo.integer"); + + assertThat(node, notNullValue()); + assertThat(node.isPresent(), equalTo(false)); + } + + @Test + public void asOptionalNodeShouldReturnEmptyOnOnMissingChildNode() { + + Optional node = asOptionalNode(testNode, "nested.foo"); + + assertThat(node, notNullValue()); + assertThat(node.isPresent(), equalTo(false)); + } + + @Test + public void asOptionalNodeShouldReturnEmptyOnOnNestedNullNode() { + + Optional node = asOptionalNode(testNode, "nested.empty"); + + assertThat(node, notNullValue()); + assertThat(node.isPresent(), equalTo(false)); + } + + @Test + public void asOptionalNodeShouldReturnNestedNodeWhenPresent() { + + Optional node = asOptionalNode(testNode, "nested.string"); + + assertThat(node, notNullValue()); + assertThat(node.isPresent(), equalTo(true)); + assertThat(node.get().isMissingNode(), equalTo(false)); + assertThat(node.get().asText(), equalTo("hi")); + } + + @Test + public void asOptionalStringShouldReturnEmptyOnMissingNode() { + + Optional value = asOptionalString(testNode, "foo"); + + assertThat(value, notNullValue()); + assertThat(value.isPresent(), equalTo(false)); + } + + @Test + public void asOptionalStringShouldReturnEmptyOnNullNode() { + + Optional value = asOptionalString(testNode, "empty"); + + assertThat(value, notNullValue()); + assertThat(value.isPresent(), equalTo(false)); + } + + @Test + public void asOptionalStringShouldReturnEmptyOnMismatchedNode() { + + Optional value = asOptionalString(testNode, "number"); + + assertThat(value, notNullValue()); + assertThat(value.isPresent(), equalTo(false)); + } + + @Test + public void asOptionalStringShouldReturnStringWhenPresent() { + + Optional value = asOptionalString(testNode, "string"); + + assertThat(value, notNullValue()); + assertThat(value.isPresent(), equalTo(true)); + assertThat(value.get(), equalTo("hi")); + } + + @Test + public void asOptionalBooleanShouldReturnEmptyOnMissingNode() { + + Optional value = asOptionalBoolean(testNode, "foo"); + + assertThat(value, notNullValue()); + assertThat(value.isPresent(), equalTo(false)); + } + + @Test + public void asOptionalBooleanShouldReturnEmptyOnNullNode() { + + Optional value = asOptionalBoolean(testNode, "empty"); + + assertThat(value, notNullValue()); + assertThat(value.isPresent(), equalTo(false)); + } + + @Test + public void asOptionalBooleanShouldReturnEmptyOnMismatchedNode() { + + Optional value = asOptionalBoolean(testNode, "number"); + + assertThat(value, notNullValue()); + assertThat(value.isPresent(), equalTo(false)); + } + + @Test + public void asOptionalBooleanShouldReturnBooleanWhenPresent() { + + Optional value = asOptionalBoolean(testNode, "boolean"); + + assertThat(value, notNullValue()); + assertThat(value.isPresent(), equalTo(true)); + assertThat(value.get(), equalTo(true)); + } + + @Test + public void asOptionalOffsetDateTimeShouldReturnEmptyOnMissingNode() { + + Optional value = asOptionalOffsetDateTime(testNode, "foo"); + + assertThat(value, notNullValue()); + assertThat(value.isPresent(), equalTo(false)); + } + + @Test + public void asOptionalOffsetDateTimeShouldReturnEmptyOnNullNode() { + + Optional value = asOptionalOffsetDateTime(testNode, "empty"); + + assertThat(value, notNullValue()); + assertThat(value.isPresent(), equalTo(false)); + } + + @Test + public void asOptionalOffsetDateTimeShouldReturnEmptyOnMismatchedNode() { + + Optional value = asOptionalOffsetDateTime(testNode, "number"); + + assertThat(value, notNullValue()); + assertThat(value.isPresent(), equalTo(false)); + } + + @Test + public void asOptionalOffsetDateTimeShouldReturnEmptyOnMalformedNode() { + + Optional value = asOptionalOffsetDateTime(testNode, "string"); + + assertThat(value, notNullValue()); + assertThat(value.isPresent(), equalTo(false)); + } + + @Test + public void asOptionalOffsetDateTimeShouldReturnDateTimeWhenPresent() { + + Optional value = asOptionalOffsetDateTime(testNode, "date_time"); + + assertThat(value, notNullValue()); + assertThat(value.isPresent(), equalTo(true)); + assertThat(value.get(), equalTo(OffsetDateTime.of(2014, 1, 1, 12, 15, 4, 0, ZoneOffset.ofHours(2)))); + } + + @Test + public void asOptionalLocalDateTimeShouldReturnEmptyOnMissingNode() { + + Optional value = asOptionalLocalDateTime(testNode, "foo"); + + assertThat(value, notNullValue()); + assertThat(value.isPresent(), equalTo(false)); + } + + @Test + public void asOptionalLocalDateTimeShouldReturnEmptyOnNullNode() { + + Optional value = asOptionalLocalDateTime(testNode, "empty"); + + assertThat(value, notNullValue()); + assertThat(value.isPresent(), equalTo(false)); + } + + @Test + public void asOptionalLocalDateTimeShouldReturnEmptyOnMismatchedNode() { + + Optional value = asOptionalLocalDateTime(testNode, "number"); + + assertThat(value, notNullValue()); + assertThat(value.isPresent(), equalTo(false)); + } + + @Test + public void asOptionalLocalDateTimeShouldReturnEmptyOnMalformedNode() { + + Optional value = asOptionalLocalDateTime(testNode, "string"); + + assertThat(value, notNullValue()); + assertThat(value.isPresent(), equalTo(false)); + } + + @Test + public void asOptionalLocalDateTimeShouldReturnDateTimeWhenPresent() { + + Optional value = asOptionalLocalDateTime(testNode, "local_date_time"); + + assertThat(value, notNullValue()); + assertThat(value.isPresent(), equalTo(true)); + assertThat(value.get(), equalTo(LocalDateTime.of(2014, 1, 1, 12, 15, 4, 0))); + } + + @Test + public void asOptionalLocalDateTimeShouldReturnCustomDateTimeWhenPresent() { + + DateTimeFormatter formatter = DateTimeFormatter.ofPattern("EEE, d MMM yyyy HH:mm:ss"); + + Optional value = asOptionalLocalDateTime(testNode, "custom_local_date_time", formatter); + + assertThat(value, notNullValue()); + assertThat(value.isPresent(), equalTo(true)); + assertThat(value.get(), equalTo(LocalDateTime.of(2014, 8, 1, 6, 53, 5, 0))); + } + + @Test + public void asOptionalDoubleShouldReturnEmptyOnMissingNode() { + + Optional value = asOptionalDouble(testNode, "foo"); + + assertThat(value, notNullValue()); + assertThat(value.isPresent(), equalTo(false)); + } + + @Test + public void asOptionalDoubleShouldReturnEmptyOnNullNode() { + + Optional value = asOptionalDouble(testNode, "empty"); + + assertThat(value, notNullValue()); + assertThat(value.isPresent(), equalTo(false)); + } + + @Test + public void asOptionalDoubleShouldReturnEmptyOnMismatchedNode() { + + Optional value = asOptionalDouble(testNode, "string"); + + assertThat(value, notNullValue()); + assertThat(value.isPresent(), equalTo(false)); + } + + @Test + public void asOptionalDoubleShouldReturnDoubleWhenPresent() { + + Optional value = asOptionalDouble(testNode, "number"); + + assertThat(value, notNullValue()); + assertThat(value.isPresent(), equalTo(true)); + assertThat(value.get(), equalTo(2.3)); + } + + @Test + public void asOptionalDoubleShouldReturnDoubleWhenIntegerIsPresent() { + + Optional value = asOptionalDouble(testNode, "integer"); + + assertThat(value, notNullValue()); + assertThat(value.isPresent(), equalTo(true)); + assertThat(value.get(), equalTo(2d)); + } + + @Test + public void asOptionalLongShouldReturnEmptyOnMissingNode() { + + Optional value = asOptionalLong(testNode, "foo"); + + assertThat(value, notNullValue()); + assertThat(value.isPresent(), equalTo(false)); + } + + @Test + public void asOptionalLongShouldReturnEmptyOnNullNode() { + + Optional value = asOptionalLong(testNode, "empty"); + + assertThat(value, notNullValue()); + assertThat(value.isPresent(), equalTo(false)); + } + + @Test + public void asOptionalLongShouldReturnEmptyOnMismatchedNode() { + + Optional value = asOptionalLong(testNode, "number"); + + assertThat(value, notNullValue()); + assertThat(value.isPresent(), equalTo(false)); + } + + @Test + public void asOptionalLongShouldReturnLongWhenPresent() { + + Optional value = asOptionalLong(testNode, "integer"); + + assertThat(value, notNullValue()); + assertThat(value.isPresent(), equalTo(true)); + assertThat(value.get(), equalTo(2L)); + } + + @Test + public void asOptionalIntegerShouldReturnEmptyOnMissingNode() { + + Optional value = asOptionalInteger(testNode, "foo"); + + assertThat(value, notNullValue()); + assertThat(value.isPresent(), equalTo(false)); + } + + @Test + public void asOptionalIntegerShouldReturnEmptyOnNullNode() { + + Optional value = asOptionalInteger(testNode, "empty"); + + assertThat(value, notNullValue()); + assertThat(value.isPresent(), equalTo(false)); + } + + @Test + public void asOptionalIntegerShouldReturnEmptyOnMismatchedNode() { + + Optional value = asOptionalInteger(testNode, "number"); + + assertThat(value, notNullValue()); + assertThat(value.isPresent(), equalTo(false)); + } + + @Test + public void asOptionalIntegerShouldReturnIntegerWhenPresent() { + + Optional value = asOptionalInteger(testNode, "integer"); + + assertThat(value, notNullValue()); + assertThat(value.isPresent(), equalTo(true)); + assertThat(value.get(), equalTo(2)); + } +} \ No newline at end of file diff --git a/run-dockerized.sh b/run-dockerized.sh new file mode 100755 index 00000000..21df946e --- /dev/null +++ b/run-dockerized.sh @@ -0,0 +1,78 @@ +#!/bin/bash + +BASEDIR=`pwd` + +isNpmPackageInstalled() { + npm list --depth 1 -g $1 > /dev/null 2>&1 +} + +# check dependencies +if ! hash "npm" 2>/dev/null; +then + echo "npm can't be found" + exit 1 +fi + +if ! hash "docker-compose" 2>/dev/null; +then + echo "docker-compose can't be found" + exit 1 +fi + +# check for Docker Compose tooling and update configuration +. update-compose-files.sh + +# remove the symlink which may have been created by running natively earlier +rm -f ${BASEDIR}/shim-server/src/main/resources/public #CMD + +# build the console +echo -n "Do you want to rebuild the console (y/N)? " +read answer +if echo "$answer" | grep -iq "^y" ;then + cd ${BASEDIR}/shim-server-ui #CMD + + if ! isNpmPackageInstalled grunt-cli + then + echo Installing Grunt. You may be asked for your password to run sudo... + sudo npm install -g grunt-cli #CMD + else + echo Grunt is already installed, skipping... + fi + + if ! isNpmPackageInstalled bower + then + echo Installing Bower. You may be asked for your password to run sudo... + sudo npm install -g bower #CMD + else + echo Bower is already installed, skipping... + fi + + echo Installing npm dependencies... + npm install #CMD + + echo Installing Bower dependencies... + bower install #CMD + + echo Building the console... + grunt build #CMD +fi + +# build the backend +echo -n "Do you want to rebuild the resource server (y/N)? " +read answer +if echo "$answer" | grep -iq "^y" ;then + echo Building the resource server... + cd ${BASEDIR} #CMD + ./gradlew build #CMD +fi + +# run the containers +cd ${BASEDIR} #CMD +echo Building the containers... +docker-compose -f docker-compose-build.yml build #CMD + +echo Starting the containers in the background... +docker-compose -f docker-compose-build.yml up -d #CMD + +echo Done, containers are starting up and may take up to a minute to be ready. + diff --git a/run-natively.sh b/run-natively.sh new file mode 100755 index 00000000..4792b20d --- /dev/null +++ b/run-natively.sh @@ -0,0 +1,66 @@ +#!/bin/bash + +BASEDIR=`pwd` + +isNpmPackageInstalled() { + npm list --depth 1 -g $1 > /dev/null 2>&1 +} + +# check dependencies +if ! hash "npm" 2>/dev/null; +then + echo "npm can't be found" + exit 1 +fi + +# build the console +echo -n "Do you want to rebuild the console (y/N)? " +read answer +if echo "$answer" | grep -iq "^y" ;then + cd ${BASEDIR}/shim-server-ui #CMD + + if ! isNpmPackageInstalled grunt-cli + then + echo Installing Grunt, you may be asked for your password to run sudo... + sudo npm install -g grunt-cli #CMD + else + echo Grunt is already installed, skipping... + fi + + if ! isNpmPackageInstalled bower + then + echo Installing Bower, you may be asked for your password to run sudo... + sudo npm install -g bower #CMD + else + echo Bower is already installed, skipping... + fi + + echo Installing npm dependencies... + npm install #CMD + + echo Installing Bower dependencies... + bower install #CMD + + echo Building the console... + grunt build #CMD + + cd ${BASEDIR}/shim-server/src/main/resources #CMD + ln -sfh ../../../../shim-server-ui/docker/assets public + #CMD create a symlink called shim-server/src/main/resources/public to the Grunt output directory +fi + +echo "The MongoDB hostname defaults to the setting in application.yaml. Initially the host name is 'mongo'." +echo -n "Please enter a hostname to override it, or press Enter to keep the default? " +read answer +trimmed=${answer// /} + +# start the resource server +echo Starting the resource server... +cd ${BASEDIR} +if [[ ! -z "$trimmed" ]] ;then + SPRING_DATA_MONGODB_URI="mongodb://${trimmed}:27017/omh_dsu" ./gradlew shim-server:bootRun #CMD + else + ./gradlew shim-server:bootRun #CMD +fi + + diff --git a/settings.gradle b/settings.gradle index 490c1b58..e85613bf 100644 --- a/settings.gradle +++ b/settings.gradle @@ -1,5 +1,4 @@ -rootProject.name = 'omh-shims' -include 'java-schema-objects' +rootProject.name = 'shimmer' include 'java-shim-sdk' include 'shim-server' diff --git a/shim-server-ui/.gitignore b/shim-server-ui/.gitignore index 2e67f280..9e3e49c9 100644 --- a/shim-server-ui/.gitignore +++ b/shim-server-ui/.gitignore @@ -1,7 +1,7 @@ .idea/ shim-server-ui.iml node_modules -dist +docker/assets .tmp .sass-cache app/bower_components diff --git a/shim-server-ui/Gruntfile.js b/shim-server-ui/Gruntfile.js index a0090175..3332cfd9 100644 --- a/shim-server-ui/Gruntfile.js +++ b/shim-server-ui/Gruntfile.js @@ -27,7 +27,7 @@ module.exports = function (grunt) { yeoman: { // configurable paths app: require('./bower.json').appPath || 'app', - dist: 'dist' + dist: 'docker/assets' }, // Watches files for changes and runs tasks based on the changed files @@ -334,7 +334,7 @@ module.exports = function (grunt) { replace: { css_relative_paths: { overwrite: true, // overwrite matched source files - src: ['dist/styles/*.vendor.css','dist/styles/*.main.css'], + src: ['<%= yeoman.dist %>/styles/*.vendor.css', '<%= yeoman.dist %>/styles/*.main.css'], replacements: [ { from: '/bower_components/bootstrap/dist/fonts', @@ -344,7 +344,7 @@ module.exports = function (grunt) { }, api_settings: { overwrite: true, // overwrite matched source files - src: ['dist/scripts/*.scripts.js'], + src: ['<%= yeoman.dist %>/scripts/*.scripts.js'], replacements: [ {from: '"/omh-shims-api";', to: '"";'} ] diff --git a/shim-server-ui/app/favicon.ico b/shim-server-ui/app/favicon.ico index 65279053..e865128b 100644 Binary files a/shim-server-ui/app/favicon.ico and b/shim-server-ui/app/favicon.ico differ diff --git a/shim-server-ui/app/scripts/controllers/main.js b/shim-server-ui/app/scripts/controllers/main.js index c40e54fc..8f49c8cd 100644 --- a/shim-server-ui/app/scripts/controllers/main.js +++ b/shim-server-ui/app/scripts/controllers/main.js @@ -36,8 +36,7 @@ angular.module('sandboxConsoleApp') * components within the UI's templates */ $scope.getHtmlId = function(username){ - var cleanUsername = username.replace(/\W+/g, "_"); - return cleanUsername; + return username.replace(/\W+/g, "_"); }; /** @@ -247,7 +246,7 @@ angular.module('sandboxConsoleApp') var url = API_ROOT_URL + "/data/" + shimKey + "/" + endPoint + "?" + "username=" + record.username + "&dateStart=" + fromDate + "&dateEnd=" + toDate - + (doNormalize ? "&normalize=true" : ""); + + (doNormalize ? "&normalize=true" : "&normalize=false"); console.info("The URL to be used is: ", url); diff --git a/shim-server-ui/docker/Dockerfile b/shim-server-ui/docker/Dockerfile new file mode 100644 index 00000000..da0c68f3 --- /dev/null +++ b/shim-server-ui/docker/Dockerfile @@ -0,0 +1,5 @@ +FROM nginx +MAINTAINER Emerson Farrugia + +COPY assets /usr/share/nginx/html +COPY nginx.conf /etc/nginx/nginx.conf \ No newline at end of file diff --git a/shim-server-ui/docker/nginx.conf b/shim-server-ui/docker/nginx.conf new file mode 100644 index 00000000..912c4116 --- /dev/null +++ b/shim-server-ui/docker/nginx.conf @@ -0,0 +1,32 @@ +http { + include mime.types; + + gzip on; + gzip_comp_level 5; + gzip_min_length 1000; + gzip_types application/json application/javascript text/css; + + server { + listen 8083 default_server; + + location = / { + root /usr/share/nginx/html; + } + + location ~* \.(eot|svg|ttf|woff|png|gif|ico|js|css|html|htaccess)$ { + root /usr/share/nginx/html; + } + + location / { + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_pass http://resource-server:8083; + } + } +} + +events { + worker_connections 512; +} diff --git a/shim-server-ui/package.json b/shim-server-ui/package.json index a9d80512..f516f076 100644 --- a/shim-server-ui/package.json +++ b/shim-server-ui/package.json @@ -3,35 +3,35 @@ "version": "0.2.1", "dependencies": {}, "devDependencies": { - "grunt": "~0.4.1", - "grunt-autoprefixer": "~0.4.0", - "grunt-bower-install": "~1.0.0", - "grunt-concurrent": "~0.5.0", - "grunt-contrib-clean": "~0.5.0", - "grunt-contrib-concat": "~0.3.0", - "grunt-contrib-connect": "~0.5.0", - "grunt-contrib-copy": "~0.4.1", - "grunt-contrib-cssmin": "~0.7.0", - "grunt-contrib-htmlmin": "~0.1.3", - "grunt-contrib-imagemin": "~0.3.0", - "grunt-contrib-jshint": "~0.7.1", - "grunt-contrib-uglify": "~0.2.0", - "grunt-contrib-watch": "~0.5.2", + "grunt": "~0.4.5", + "grunt-autoprefixer": "~3.0.0", + "grunt-bower-install": "~1.6.0", + "grunt-concurrent": "~1.0.0", + "grunt-contrib-clean": "~0.6.0", + "grunt-contrib-concat": "~0.5.1", + "grunt-contrib-connect": "~0.10.1", + "grunt-contrib-copy": "~0.8.0", + "grunt-contrib-cssmin": "~0.12.3", + "grunt-contrib-htmlmin": "~0.4.0", + "grunt-contrib-imagemin": "~0.9.4", + "grunt-contrib-jshint": "~0.11.2", + "grunt-contrib-uglify": "~0.9.1", + "grunt-contrib-watch": "~0.6.1", "grunt-connect-proxy": "~0.2.0", - "grunt-google-cdn": "~0.2.0", - "grunt-newer": "~0.6.1", - "grunt-ngmin": "~0.0.2", + "grunt-google-cdn": "~0.4.3", + "grunt-newer": "~1.1.0", + "grunt-ngmin": "~0.0.3", "grunt-rev": "~0.1.0", - "grunt-svgmin": "~0.2.0", - "grunt-usemin": "~2.0.0", - "jshint-stylish": "~0.1.3", - "load-grunt-tasks": "~0.4.0", - "time-grunt": "~0.2.1", - "grunt-karma": "~0.9.0", + "grunt-svgmin": "~2.0.1", + "grunt-usemin": "~3.0.0", + "jshint-stylish": "~2.0.0", + "load-grunt-tasks": "~3.2.0", + "time-grunt": "~1.2.1", + "grunt-karma": "~0.10.1", "karma-phantomjs-launcher": "~0.1.4", - "karma": "~0.12.23", - "karma-jasmine": "~0.1.5", - "grunt-text-replace": "~0.3.12" + "karma": "~0.12.33", + "karma-jasmine": "~0.3.5", + "grunt-text-replace": "~0.4.0" }, "engines": { "node": ">=0.10.0" diff --git a/shim-server/build.gradle b/shim-server/build.gradle index e318b4e1..07199db0 100644 --- a/shim-server/build.gradle +++ b/shim-server/build.gradle @@ -1,24 +1,26 @@ buildscript { repositories { mavenLocal() - mavenCentral() + jcenter() } ext { - springBootVersion = '1.2.1.RELEASE' + springBootVersion = "1.2.5.RELEASE" } dependencies { + classpath 'io.spring.gradle:dependency-management-plugin:0.5.3.RELEASE' classpath "org.springframework.boot:spring-boot-gradle-plugin:${springBootVersion}" } } apply plugin: 'spring-boot' +apply plugin: 'io.spring.dependency-management' -version = shimServerVersion +version = shimmerVersion jar { - baseName = 'shim-server' + baseName = 'shimmer' } bootRepackage { @@ -26,48 +28,63 @@ bootRepackage { } ext { - healthVaultSdkVersion = '1.6' signpostVersion = '1.2.1.2' - springSecurityOAuthVersion = '2.0.5.RELEASE' + springSecurityOAuthVersion = '2.0.7.RELEASE' } +ext['jackson.version'] = '2.5.3' +ext["spring-data-releasetrain.version"] = "Fowler-SR2" + configurations { compile.exclude module: "spring-boot-starter-tomcat" } +dependencyManagement { + imports { + mavenBom "org.springframework.boot:spring-boot-dependencies:${springBootVersion}" + } +} + dependencies { - compile project(':java-schema-objects') compile project(':java-shim-sdk') compile "commons-io:commons-io:2.4" + compile "com.google.code.findbugs:jsr305:3.0.0" compile "org.apache.httpcomponents:httpclient" compile "org.apache.httpcomponents:httpcore:4.3.3" - compile "com.microsoft.hsg:hv-jaxb:${healthVaultSdkVersion}" - compile "com.microsoft.hsg:hv-sdk:${healthVaultSdkVersion}" compile "com.fasterxml.jackson.core:jackson-annotations" compile "com.fasterxml.jackson.dataformat:jackson-dataformat-xml" - compile "joda-time:joda-time" - compile "com.jayway.jsonpath:json-path" - compile "com.github.fge:json-schema-validator:2.2.6" + compile "com.fasterxml.jackson.datatype:jackson-datatype-jsr310" + compile "com.fasterxml.jackson.datatype:jackson-datatype-jdk8" + compile "joda-time:joda-time" // TODO delete this when shims have been migrated + compile "org.mongodb:mongo-java-driver" + compile "org.openmhealth.schema:omh-schema-sdk:${omhSchemaSdkVersion}" compile "oauth.signpost:signpost-commonshttp4:${signpostVersion}" compile "oauth.signpost:signpost-core:${signpostVersion}" compile "org.springframework.boot:spring-boot-autoconfigure" - compile "org.springframework.boot:spring-boot-starter-data-mongodb" compile "org.springframework.boot:spring-boot-starter-jetty" - compile "org.springframework.boot:spring-boot-starter-web" + compile "org.springframework.data:spring-data-mongodb" compile "org.springframework.security.oauth:spring-security-oauth:${springSecurityOAuthVersion}" compile "org.springframework.security.oauth:spring-security-oauth2:${springSecurityOAuthVersion}" - compile "org.codehaus.woodstox:woodstox-core-asl:4.2.1" + compile "org.springframework:spring-web" + compile "org.springframework:spring-webmvc" + compile "org.codehaus.woodstox:woodstox-core-asl:4.4.1" // required to print XML + testCompile 'org.testng:testng:6.8.21' // slowly migrate to TestNG testCompile "junit:junit" testCompile "org.mockito:mockito-core" testCompile "org.springframework.boot:spring-boot-starter-test" + + runtime 'org.slf4j:jcl-over-slf4j' + runtime 'org.slf4j:log4j-over-slf4j' + runtime 'ch.qos.logback:logback-classic' } -task copyArchiveJarToDockerContext(dependsOn: build, type: Copy) { +task copyArchiveJarToDockerContext(dependsOn: assemble, type: Copy) { from 'build/libs' - into '../docker/binary' + into 'docker' include "${jar.archiveName}" rename { String fileName -> fileName.replace("${jar.archiveName}", "${jar.baseName}.jar") } -} \ No newline at end of file +} +build.dependsOn copyArchiveJarToDockerContext diff --git a/shim-server/docker/Dockerfile b/shim-server/docker/Dockerfile new file mode 100644 index 00000000..50d14fd0 --- /dev/null +++ b/shim-server/docker/Dockerfile @@ -0,0 +1,10 @@ +FROM java:openjdk-8-jre +MAINTAINER Emerson Farrugia + +ENV SERVER_PREFIX /opt/omh/shimmer + +RUN mkdir -p $SERVER_PREFIX +ADD shimmer.jar $SERVER_PREFIX/ +EXPOSE 8083 + +CMD /usr/bin/java -jar $SERVER_PREFIX/shimmer.jar --spring.config.location=file:$SERVER_PREFIX/ diff --git a/shim-server/src/main/java/org/openmhealth/shim/AccessParameterClientTokenServices.java b/shim-server/src/main/java/org/openmhealth/shim/AccessParameterClientTokenServices.java index 7fce4072..c870a965 100644 --- a/shim-server/src/main/java/org/openmhealth/shim/AccessParameterClientTokenServices.java +++ b/shim-server/src/main/java/org/openmhealth/shim/AccessParameterClientTokenServices.java @@ -1,11 +1,11 @@ /* - * Copyright 2014 Open mHealth + * 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 + * 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, @@ -62,8 +62,9 @@ public void saveAccessToken(OAuth2ProtectedResourceDetails resource, username, shimKey, new Sort(Sort.Direction.DESC, "dateCreated")); if (accessParameters == null) { - throw new IllegalStateException("Can't save serialized spring oauth2 access token, " + - "no corresponding access parameters entity was found in which to put it."); + accessParameters = new AccessParameters(); + accessParameters.setUsername(username); + accessParameters.setShimKey(shimKey); } accessParameters.setSerializedToken(SerializationUtils.serialize(accessToken)); diff --git a/shim-server/src/main/java/org/openmhealth/shim/AccessParametersRepo.java b/shim-server/src/main/java/org/openmhealth/shim/AccessParametersRepo.java index a5b3de87..c3577762 100644 --- a/shim-server/src/main/java/org/openmhealth/shim/AccessParametersRepo.java +++ b/shim-server/src/main/java/org/openmhealth/shim/AccessParametersRepo.java @@ -1,11 +1,11 @@ /* - * Copyright 2014 Open mHealth + * 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 + * 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, @@ -18,14 +18,13 @@ import org.springframework.data.domain.Sort; import org.springframework.data.mongodb.repository.MongoRepository; -import org.springframework.stereotype.Repository; import java.util.List; + /** * @author Danilo Bonilla */ -@Repository public interface AccessParametersRepo extends MongoRepository { AccessParameters findByUsernameAndShimKey(String username, String shimKey, Sort sort); diff --git a/shim-server/src/main/java/org/openmhealth/shim/Application.java b/shim-server/src/main/java/org/openmhealth/shim/Application.java index 71017524..e8f7ec6d 100644 --- a/shim-server/src/main/java/org/openmhealth/shim/Application.java +++ b/shim-server/src/main/java/org/openmhealth/shim/Application.java @@ -1,11 +1,11 @@ /* - * Copyright 2014 Open mHealth + * 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 + * 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, @@ -16,11 +16,11 @@ package org.openmhealth.shim; -import org.joda.time.format.DateTimeFormat; -import org.joda.time.format.DateTimeFormatter; +import com.fasterxml.jackson.databind.ObjectMapper; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.EnableAutoConfiguration; +import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.ComponentScan; import org.springframework.context.annotation.Configuration; import org.springframework.data.domain.Sort; @@ -28,19 +28,30 @@ import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; import org.springframework.security.core.context.SecurityContextHolder; -import org.springframework.web.bind.annotation.*; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; +import java.time.LocalDate; import java.util.*; +import static java.time.ZoneOffset.UTC; +import static java.util.Collections.singletonList; +import static org.openmhealth.schema.configuration.JacksonConfiguration.newObjectMapper; +import static org.springframework.http.MediaType.APPLICATION_JSON_VALUE; +import static org.springframework.web.bind.annotation.RequestMethod.*; + + /** * @author Danilo Bonilla */ @Configuration @EnableAutoConfiguration -@ComponentScan(basePackages = "org.openmhealth.*") +@ComponentScan(basePackages = "org.openmhealth") @EnableWebSecurity @RestController public class Application extends WebSecurityConfigurerAdapter { @@ -57,8 +68,7 @@ public class Application extends WebSecurityConfigurerAdapter { @Autowired private ShimRegistry shimRegistry; - private static DateTimeFormatter formatterDate = DateTimeFormat.forPattern("yyyy-MM-dd"); - + // TODO clarify what this is for private static final String REDIRECT_OOB = "oob"; public static void main(String[] args) { @@ -71,21 +81,17 @@ protected void configure(HttpSecurity http) throws Exception { * Allow full anonymous authentication. */ http.csrf().disable() - .authorizeRequests().anyRequest().permitAll(); + .authorizeRequests().anyRequest().permitAll(); } /** * Return shims available in the registry and all endpoints. * - * @return - list of shims + endpoints in a map. - * @throws ShimException + * @return list of shims + endpoints in a map. */ - @RequestMapping("registry") - public - @ResponseBody - List> shimList( - @RequestParam(value = "available", defaultValue = "") String available - ) throws ShimException { + @RequestMapping(value = "registry", produces = APPLICATION_JSON_VALUE) + public List> shimList(@RequestParam(value = "available", defaultValue = "") String available) + throws ShimException { List> results = new ArrayList<>(); List shims = "".equals(available) ? shimRegistry.getShims() : shimRegistry.getAvailableShims(); @@ -99,11 +105,12 @@ List> shimList( row.put("shimKey", shim.getShimKey()); row.put("label", shim.getLabel()); row.put("endpoints", endpoints); - if (shim.getClientId() != null) { - row.put("clientId", shim.getClientId()); + ApplicationAccessParameters parameters = shim.findApplicationAccessParameters(); + if (parameters.getClientId() != null) { + row.put("clientId", parameters.getClientId()); } - if (shim.getClientSecret() != null) { - row.put("clientSecret", shim.getClientSecret()); + if (parameters.getClientSecret() != null) { + row.put("clientSecret", parameters.getClientSecret()); } results.add(row); } @@ -113,20 +120,17 @@ List> shimList( /** * Update shim configuration * - * @return - list of shims + endpoints in a map. - * @throws ShimException + * @return list of shims + endpoints in a map. */ - @RequestMapping(value = "shim/{shim}/config", - method = {RequestMethod.GET, RequestMethod.PUT, RequestMethod.POST}) - public - @ResponseBody - List updateShimConfig( - @PathVariable("shim") String shimKey, - @RequestParam("clientId") String clientId, - @RequestParam("clientSecret") String clientSecret - ) throws ShimException { - ApplicationAccessParameters parameters = - applicationAccessParametersRepo.findByShimKey(shimKey); + @RequestMapping(value = "shim/{shim}/config", method = {GET, PUT, POST}, produces = APPLICATION_JSON_VALUE) + public List updateShimConfig( + @PathVariable("shim") String shimKey, + @RequestParam("clientId") String clientId, + @RequestParam("clientSecret") String clientSecret) + throws ShimException { + + ApplicationAccessParameters parameters = applicationAccessParametersRepo.findByShimKey(shimKey); + if (parameters == null) { parameters = new ApplicationAccessParameters(); parameters.setShimKey(shimKey); @@ -135,29 +139,27 @@ List updateShimConfig( parameters.setClientSecret(clientSecret); applicationAccessParametersRepo.save(parameters); shimRegistry.init(); - return Arrays.asList("success"); + + return singletonList("success"); } /** * Retrieve access parameters for the given username/fragment. * - * @param username - username fragment to search. + * @param username username fragment to search. * @return List of access parameters. - * @throws ShimException */ - @RequestMapping("authorizations") - public - @ResponseBody - List> authorizations( - @RequestParam(value = "username") String username) throws ShimException { - List accessParameters = - accessParametersRepo.findAllByUsernameLike(username); + @RequestMapping(value = "authorizations", produces = APPLICATION_JSON_VALUE) + public List> authorizations(@RequestParam(value = "username") String username) + throws ShimException { + + List accessParameters = accessParametersRepo.findAllByUsernameLike(username); List> results = new ArrayList<>(); Map> auths = new HashMap<>(); for (AccessParameters accessParameter : accessParameters) { if (!auths.containsKey(accessParameter.getUsername())) { - auths.put(accessParameter.getUsername(), new HashSet()); + auths.put(accessParameter.getUsername(), new HashSet<>()); } auths.get(accessParameter.getUsername()).add(accessParameter.getShimKey()); } @@ -173,25 +175,21 @@ List> authorizations( /** * Endpoint for triggering domain approval. * - * @param username - The user record for which we're authorizing a shim. - * @param clientRedirectUrl - The URL to which the external shim client (i.e, hibpone) will - * be redirected after authorization is complete. - * @param shim - The shim registry key of the shim we're approving - * @return - AuthorizationRequest parameters, including a boolean - * flag if already authorized. + * @param username The user record for which we're authorizing a shim. + * @param clientRedirectUrl The URL to which the external shim client will be redirected after authorization is + * complete. + * @param shim The shim registry key of the shim we're approving + * @return AuthorizationRequest parameters, including a boolean flag if already authorized. */ - @RequestMapping("/authorize/{shim}") - public - @ResponseBody - AuthorizationRequestParameters authorize(@RequestParam(value = "username") String username, - @RequestParam(value = "client_redirect_url", - defaultValue = REDIRECT_OOB) - String clientRedirectUrl, - @PathVariable("shim") String shim) throws ShimException { + @RequestMapping(value = "/authorize/{shim}", produces = APPLICATION_JSON_VALUE) + public AuthorizationRequestParameters authorize( + @RequestParam(value = "username") String username, + @RequestParam(value = "client_redirect_url", defaultValue = REDIRECT_OOB) String clientRedirectUrl, + @PathVariable("shim") String shim) throws ShimException { + setPassThroughAuthentication(username, shim); - AuthorizationRequestParameters authParams = - shimRegistry.getShim(shim).getAuthorizationRequestParameters( - username, Collections.emptyMap()); + AuthorizationRequestParameters authParams = shimRegistry.getShim(shim) + .getAuthorizationRequestParameters(username, Collections.emptyMap()); /** * Save authorization parameters to local repo. They will be * re-fetched via stateKey upon approval. @@ -205,46 +203,44 @@ AuthorizationRequestParameters authorize(@RequestParam(value = "username") Strin /** * Endpoint for removing authorizations for a given user and shim. * - * @param username - The user record for which we're removing shim access. - * @param shim - The shim registry key of the shim authorization we're removing. - * @return - Simple response message. + * @param username The user record for which we're removing shim access. + * @param shim The shim registry key of the shim authorization we're removing. + * @return Simple response message. */ - @RequestMapping(value = "/de-authorize/{shim}", method = RequestMethod.DELETE) - public - @ResponseBody - List removeAuthorization(@RequestParam(value = "username") String username, - @PathVariable("shim") String shim) throws ShimException { - List accessParameters = - accessParametersRepo.findAllByUsernameAndShimKey(username, shim); - for (AccessParameters accessParameter : accessParameters) { - accessParametersRepo.delete(accessParameter); - } - return Arrays.asList("Success: Authorization Removed."); + @RequestMapping(value = "/de-authorize/{shim}", method = DELETE, produces = APPLICATION_JSON_VALUE) + public List removeAuthorization( + @RequestParam(value = "username") String username, + @PathVariable("shim") String shim) + throws ShimException { + + List accessParameters = accessParametersRepo.findAllByUsernameAndShimKey(username, shim); + + accessParameters.forEach(accessParametersRepo::delete); + + return singletonList("Success: Authorization Removed."); } /** * Endpoint for handling approvals from external data providers * - * @param servletRequest - Request posted by the external data provider. - * @return - AuthorizationResponse object with details - * and result: authorize, error, or denied. + * @param servletRequest Request posted by the external data provider. + * @return AuthorizationResponse object with details and result: authorize, error, or denied. */ - @RequestMapping(value = "/authorize/{shim}/callback", - method = {RequestMethod.POST, RequestMethod.GET}) - public - @ResponseBody - AuthorizationResponse approve(@PathVariable("shim") String shim, - HttpServletRequest servletRequest, - HttpServletResponse servletResponse) throws ShimException { + @RequestMapping(value = "/authorize/{shim}/callback", method = {POST, GET}, produces = APPLICATION_JSON_VALUE) + public AuthorizationResponse approve( + @PathVariable("shim") String shim, + HttpServletRequest servletRequest, + HttpServletResponse servletResponse) + throws ShimException { + String stateKey = servletRequest.getParameter("state"); AuthorizationRequestParameters authParams = authParametersRepo.findByStateKey(stateKey); if (authParams == null) { - throw new ShimException("Invalid state key, original access " + - "request not found. Cannot authorize."); - } else { + throw new ShimException("Invalid state key, original access request not found. Cannot authorize."); + } + else { setPassThroughAuthentication(authParams.getUsername(), shim); - AuthorizationResponse response = - shimRegistry.getShim(shim).handleAuthorizationResponse(servletRequest); + AuthorizationResponse response = shimRegistry.getShim(shim).handleAuthorizationResponse(servletRequest); /** * Save the access parameters to local repo. * They will be re-fetched via username and path parameters @@ -260,13 +256,13 @@ AuthorizationResponse approve(@PathVariable("shim") String shim, * the authorization response. */ if (authParams.getClientRedirectUrl() != null && - !REDIRECT_OOB.equals(authParams.getClientRedirectUrl())) { + !REDIRECT_OOB.equals(authParams.getClientRedirectUrl())) { try { servletResponse.sendRedirect(authParams.getClientRedirectUrl()); - } catch (IOException e) { + } + catch (IOException e) { e.printStackTrace(); - throw new ShimException("Error occurred redirecting to :" - + authParams.getRedirectUri()); + throw new ShimException("Error occurred redirecting to :" + authParams.getRedirectUri()); } return null; } @@ -277,59 +273,44 @@ AuthorizationResponse approve(@PathVariable("shim") String shim, /** * Endpoint for retrieving data from shims. * - * @param username - User ID record for which to retrieve data, if not - * approved this will throw ShimException. - *

- * todo: finish javadoc! - * @return - The shim data response wrapper with data from the shim. + * @param username User ID record for which to retrieve data, if not approved this will throw ShimException. + * todo: finish javadoc! + * @return The shim data response wrapper with data from the shim. */ - @RequestMapping(value = "/data/{shim}/{dataType}") - public - @ResponseBody - ShimDataResponse data(@RequestParam(value = "username") String username, - - @PathVariable("shim") String shim, - - @PathVariable("dataType") String dataTypeKey, - - @RequestParam(value = "normalize", - required = false, defaultValue = "") - String normalize, - - @RequestParam(value = "dateStart", required = false, defaultValue = "") - String dateStart, - - @RequestParam(value = "dateEnd", required = false, defaultValue = "") - String dateEnd, - - @RequestParam(value = "numToReturn", required = false, defaultValue = "50") - Long numToReturn, - - HttpServletRequest servletRequest - - ) throws ShimException { + @RequestMapping(value = "/data/{shim}/{dataType}", produces = APPLICATION_JSON_VALUE) + public ShimDataResponse data( + @RequestParam(value = "username") String username, + @PathVariable("shim") String shim, + @PathVariable("dataType") String dataTypeKey, + @RequestParam(value = "normalize", defaultValue = "") String normalize, + @RequestParam(value = "dateStart", defaultValue = "") String dateStart, + @RequestParam(value = "dateEnd", defaultValue = "") String dateEnd, + @RequestParam(value = "numToReturn", defaultValue = "50") Long numToReturn) + throws ShimException { setPassThroughAuthentication(username, shim); ShimDataRequest shimDataRequest = new ShimDataRequest(); shimDataRequest.setDataTypeKey(dataTypeKey); - shimDataRequest.setNormalize(!"".equals(normalize)); + + if(!normalize.equals("")){ + shimDataRequest.setNormalize(Boolean.parseBoolean(normalize)); + } + if (!"".equals(dateStart)) { - shimDataRequest.setStartDate(formatterDate.parseDateTime(dateStart)); + shimDataRequest.setStartDateTime(LocalDate.parse(dateStart).atStartOfDay().atOffset(UTC)); } if (!"".equals(dateEnd)) { - shimDataRequest.setEndDate(formatterDate.parseDateTime(dateEnd)); + shimDataRequest.setEndDateTime(LocalDate.parse(dateEnd).atStartOfDay().atOffset(UTC)); } shimDataRequest.setNumToReturn(numToReturn); - AccessParameters accessParameters = - accessParametersRepo.findByUsernameAndShimKey( + AccessParameters accessParameters = accessParametersRepo.findByUsernameAndShimKey( username, shim, new Sort(Sort.Direction.DESC, "dateCreated")); if (accessParameters == null) { - throw new ShimException("User '" - + username + "' has not authorized shim: '" + shim + "'"); + throw new ShimException("User '" + username + "' has not authorized shim: '" + shim + "'"); } shimDataRequest.setAccessParameters(accessParameters); return shimRegistry.getShim(shim).getData(shimDataRequest); @@ -339,7 +320,11 @@ ShimDataResponse data(@RequestParam(value = "username") String username, * Sets pass through authentication required by spring. */ private void setPassThroughAuthentication(String username, String shim) { - SecurityContextHolder.getContext() - .setAuthentication(new ShimAuthentication(username, shim)); + SecurityContextHolder.getContext().setAuthentication(new ShimAuthentication(username, shim)); + } + + @Bean + public ObjectMapper objectMapper() { + return newObjectMapper(); } } diff --git a/shim-server/src/main/java/org/openmhealth/shim/ApplicationAccessParameters.java b/shim-server/src/main/java/org/openmhealth/shim/ApplicationAccessParameters.java index d1e221e5..c5f0b9b3 100644 --- a/shim-server/src/main/java/org/openmhealth/shim/ApplicationAccessParameters.java +++ b/shim-server/src/main/java/org/openmhealth/shim/ApplicationAccessParameters.java @@ -1,11 +1,11 @@ /* - * Copyright 2014 Open mHealth + * 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 + * 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, @@ -33,6 +33,16 @@ public class ApplicationAccessParameters { private String clientSecret; + public ApplicationAccessParameters() { + + } + + public ApplicationAccessParameters(String shimKey, String clientId, String clientSecret) { + this.shimKey = shimKey; + this.clientId = clientId; + this.clientSecret = clientSecret; + } + public String getId() { return id; } diff --git a/shim-server/src/main/java/org/openmhealth/shim/ApplicationAccessParametersRepo.java b/shim-server/src/main/java/org/openmhealth/shim/ApplicationAccessParametersRepo.java index 6eb81723..938cb4d1 100644 --- a/shim-server/src/main/java/org/openmhealth/shim/ApplicationAccessParametersRepo.java +++ b/shim-server/src/main/java/org/openmhealth/shim/ApplicationAccessParametersRepo.java @@ -1,11 +1,11 @@ /* - * Copyright 2014 Open mHealth + * 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 + * 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, @@ -17,12 +17,11 @@ package org.openmhealth.shim; import org.springframework.data.mongodb.repository.MongoRepository; -import org.springframework.stereotype.Repository; + /** * @author Danilo Bonilla */ -@Repository public interface ApplicationAccessParametersRepo extends MongoRepository { ApplicationAccessParameters findByShimKey(String shimKey); diff --git a/shim-server/src/main/java/org/openmhealth/shim/AuthorizationRequestParameters.java b/shim-server/src/main/java/org/openmhealth/shim/AuthorizationRequestParameters.java index 7243458f..4d937108 100644 --- a/shim-server/src/main/java/org/openmhealth/shim/AuthorizationRequestParameters.java +++ b/shim-server/src/main/java/org/openmhealth/shim/AuthorizationRequestParameters.java @@ -1,11 +1,11 @@ /* - * Copyright 2014 Open mHealth + * 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 + * 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, @@ -33,6 +33,7 @@ public class AuthorizationRequestParameters { private String username; + // TODO: unused, drop private HttpMethod httpMethod = HttpMethod.POST; private String redirectUri; @@ -79,14 +80,6 @@ public void setAuthorizationUrl(String authorizationUrl) { this.authorizationUrl = authorizationUrl; } - public HttpMethod getHttpMethod() { - return httpMethod; - } - - public void setHttpMethod(HttpMethod httpMethod) { - this.httpMethod = httpMethod; - } - public String getStateKey() { return stateKey; } diff --git a/shim-server/src/main/java/org/openmhealth/shim/AuthorizationRequestParametersRepo.java b/shim-server/src/main/java/org/openmhealth/shim/AuthorizationRequestParametersRepo.java index 7b0eb7f4..b372c8f3 100644 --- a/shim-server/src/main/java/org/openmhealth/shim/AuthorizationRequestParametersRepo.java +++ b/shim-server/src/main/java/org/openmhealth/shim/AuthorizationRequestParametersRepo.java @@ -1,11 +1,11 @@ /* - * Copyright 2014 Open mHealth + * 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 + * 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, @@ -17,16 +17,14 @@ package org.openmhealth.shim; import org.springframework.data.mongodb.repository.MongoRepository; -import org.springframework.stereotype.Repository; import java.util.List; + /** * @author Danilo Bonilla */ -@Repository -public interface AuthorizationRequestParametersRepo - extends MongoRepository { +public interface AuthorizationRequestParametersRepo extends MongoRepository { List findByUsername(String username); diff --git a/shim-server/src/main/java/org/openmhealth/shim/AuthorizationResponse.java b/shim-server/src/main/java/org/openmhealth/shim/AuthorizationResponse.java index 293c8290..49b9e9cb 100644 --- a/shim-server/src/main/java/org/openmhealth/shim/AuthorizationResponse.java +++ b/shim-server/src/main/java/org/openmhealth/shim/AuthorizationResponse.java @@ -1,11 +1,11 @@ /* - * Copyright 2014 Open mHealth + * 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 + * 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, diff --git a/shim-server/src/main/java/org/openmhealth/shim/OAuth1Shim.java b/shim-server/src/main/java/org/openmhealth/shim/OAuth1Shim.java index 40353135..8a4b82cf 100644 --- a/shim-server/src/main/java/org/openmhealth/shim/OAuth1Shim.java +++ b/shim-server/src/main/java/org/openmhealth/shim/OAuth1Shim.java @@ -1,11 +1,11 @@ /* - * Copyright 2014 Open mHealth + * 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 + * 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, diff --git a/shim-server/src/main/java/org/openmhealth/shim/OAuth1ShimBase.java b/shim-server/src/main/java/org/openmhealth/shim/OAuth1ShimBase.java index 77c7aeff..52aab768 100644 --- a/shim-server/src/main/java/org/openmhealth/shim/OAuth1ShimBase.java +++ b/shim-server/src/main/java/org/openmhealth/shim/OAuth1ShimBase.java @@ -1,3 +1,19 @@ +/* + * 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; import oauth.signpost.OAuth; @@ -6,17 +22,13 @@ import org.apache.http.client.methods.HttpGet; import org.apache.http.client.methods.HttpRequestBase; import org.apache.http.impl.client.HttpClients; -import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.HttpMethod; -import org.springframework.stereotype.Component; -import org.springframework.stereotype.Service; import org.springframework.web.client.HttpClientErrorException; import javax.servlet.http.HttpServletRequest; import java.io.IOException; import java.net.URL; import java.util.HashMap; -import java.util.LinkedHashMap; import java.util.Map; /** @@ -24,7 +36,7 @@ * * @author Danilo Bonilla */ -public abstract class OAuth1ShimBase implements Shim, OAuth1Shim { +public abstract class OAuth1ShimBase extends ShimBase implements OAuth1Shim { protected HttpClient httpClient = HttpClients.createDefault(); @@ -32,8 +44,10 @@ public abstract class OAuth1ShimBase implements Shim, OAuth1Shim { private ShimServerConfig shimServerConfig; - protected OAuth1ShimBase(AuthorizationRequestParametersRepo authorizationRequestParametersRepo, + protected OAuth1ShimBase(ApplicationAccessParametersRepo applicationParametersRepo, + AuthorizationRequestParametersRepo authorizationRequestParametersRepo, ShimServerConfig shimServerConfig) { + super(applicationParametersRepo); this.authorizationRequestParametersRepo = authorizationRequestParametersRepo; this.shimServerConfig = shimServerConfig; } @@ -81,7 +95,6 @@ public AuthorizationRequestParameters getAuthorizationRequestParameters( parameters.setUsername(username); parameters.setRedirectUri(callbackUrl); parameters.setStateKey(stateKey); - parameters.setHttpMethod(HttpMethod.GET); parameters.setAuthorizationUrl(authorizeUrl.toString()); parameters.setRequestParams(tokenParameters); @@ -147,9 +160,10 @@ public AuthorizationResponse handleAuthorizationResponse(HttpServletRequest serv throw new ShimException("Access token could not be retrieved"); } + ApplicationAccessParameters parameters = findApplicationAccessParameters(); AccessParameters accessParameters = new AccessParameters(); - accessParameters.setClientId(getClientId()); - accessParameters.setClientSecret(getClientSecret()); + accessParameters.setClientId(parameters.getClientId()); + accessParameters.setClientSecret(parameters.getClientSecret()); accessParameters.setStateKey(stateKey); accessParameters.setUsername(authParams.getUsername()); accessParameters.setAccessToken(accessToken); @@ -172,10 +186,11 @@ protected HttpRequestBase getSignedRequest(String unsignedUrl, String token, String tokenSecret, Map oauthParams) throws ShimException { + ApplicationAccessParameters parameters = findApplicationAccessParameters(); return OAuth1Utils.getSignedRequest( unsignedUrl, - getClientId(), - getClientSecret(), + parameters.getClientId(), + parameters.getClientSecret(), token, tokenSecret, oauthParams); } @@ -184,10 +199,11 @@ protected URL signUrl(String unsignedUrl, String tokenSecret, Map oauthParams) throws ShimException { + ApplicationAccessParameters parameters = findApplicationAccessParameters(); return OAuth1Utils.buildSignedUrl( unsignedUrl, - getClientId(), - getClientSecret(), + parameters.getClientId(), + parameters.getClientSecret(), token, tokenSecret, oauthParams); } diff --git a/shim-server/src/main/java/org/openmhealth/shim/OAuth1Utils.java b/shim-server/src/main/java/org/openmhealth/shim/OAuth1Utils.java index 23cdc970..401b7bf6 100644 --- a/shim-server/src/main/java/org/openmhealth/shim/OAuth1Utils.java +++ b/shim-server/src/main/java/org/openmhealth/shim/OAuth1Utils.java @@ -1,11 +1,11 @@ /* - * Copyright 2014 Open mHealth + * 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 + * 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, 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 a52a67c5..0d9987fe 100644 --- a/shim-server/src/main/java/org/openmhealth/shim/OAuth2Shim.java +++ b/shim-server/src/main/java/org/openmhealth/shim/OAuth2Shim.java @@ -1,11 +1,11 @@ /* - * Copyright 2014 Open mHealth + * 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 + * 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, @@ -24,15 +24,6 @@ */ public interface OAuth2Shim { - /** - * Request parameters to be used when 'triggering' - * spring oauth2. This should be the equivalent - * of a ping to the external data provider. - * - * @return - The Shim data request to use for trigger. - */ - public abstract ShimDataRequest getTriggerDataRequest(); - public abstract OAuth2ProtectedResourceDetails getResource(); public abstract AuthorizationCodeAccessTokenProvider getAuthorizationCodeAccessTokenProvider(); diff --git a/shim-server/src/main/java/org/openmhealth/shim/OAuth2ShimBase.java b/shim-server/src/main/java/org/openmhealth/shim/OAuth2ShimBase.java index 62e422da..99bf3ce8 100644 --- a/shim-server/src/main/java/org/openmhealth/shim/OAuth2ShimBase.java +++ b/shim-server/src/main/java/org/openmhealth/shim/OAuth2ShimBase.java @@ -1,11 +1,11 @@ /* - * Copyright 2014 Open mHealth + * 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 + * 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, @@ -41,7 +41,7 @@ * * @author Danilo Bonilla */ -public abstract class OAuth2ShimBase implements Shim, OAuth2Shim { +public abstract class OAuth2ShimBase extends ShimBase implements OAuth2Shim { private AuthorizationRequestParametersRepo authorizationRequestParametersRepo; @@ -49,17 +49,16 @@ public abstract class OAuth2ShimBase implements Shim, OAuth2Shim { protected ShimServerConfig shimServerConfig; - protected OAuth2ShimBase(AuthorizationRequestParametersRepo authorizationRequestParametersRepo, + protected OAuth2ShimBase(ApplicationAccessParametersRepo applicationParametersRepo, + AuthorizationRequestParametersRepo authorizationRequestParametersRepo, AccessParametersRepo accessParametersRepo, ShimServerConfig shimServerConfig) { + super(applicationParametersRepo); this.authorizationRequestParametersRepo = authorizationRequestParametersRepo; this.accessParametersRepo = accessParametersRepo; this.shimServerConfig = shimServerConfig; } - protected abstract AuthorizationRequestParameters getAuthorizationRequestParameters( - final String username, final UserRedirectRequiredException exception); - protected abstract ResponseEntity getData( OAuth2RestOperations restTemplate, ShimDataRequest shimDataRequest) throws ShimException; @@ -88,9 +87,10 @@ public AuthorizationRequestParameters getAuthorizationRequestParameters(String u * Build an authorization request from the exception * parameters. We also serialize spring's accessTokenRequest. */ - AuthorizationRequestParameters authRequestParams = - getAuthorizationRequestParameters(username, e); - + AuthorizationRequestParameters authRequestParams = new AuthorizationRequestParameters(); + authRequestParams.setRedirectUri(e.getRedirectUri()); + authRequestParams.setStateKey(e.getStateKey()); + authRequestParams.setAuthorizationUrl(getAuthorizationUrl(e)); authRequestParams.setSerializedRequest(SerializationUtils.serialize(accessTokenRequest)); authRequestParams.setStateKey(stateKey); @@ -99,19 +99,37 @@ public AuthorizationRequestParameters getAuthorizationRequestParameters(String u } } + protected abstract String getAuthorizationUrl(UserRedirectRequiredException exception); + public OAuth2ProtectedResourceDetails getResource() { + ApplicationAccessParameters parameters = findApplicationAccessParameters(); AuthorizationCodeResourceDetails resource = new AuthorizationCodeResourceDetails(); resource.setAccessTokenUri(getBaseTokenUrl()); resource.setUserAuthorizationUri(getBaseAuthorizeUrl()); - resource.setClientId(getClientId()); + resource.setClientId(parameters.getClientId()); resource.setScope(getScopes()); - resource.setClientSecret(getClientSecret()); + resource.setClientSecret(parameters.getClientSecret()); resource.setTokenName("access_token"); resource.setGrantType("authorization_code"); resource.setUseCurrentUri(true); return resource; } + + /** + * Request parameters to be used when 'triggering' + * spring oauth2. This should be the equivalent + * of a ping to the external data provider. + * + * @return - The Shim data request to use for trigger. + */ + protected ShimDataRequest getTriggerDataRequest() { + ShimDataRequest shimDataRequest = new ShimDataRequest(); + shimDataRequest.setDataTypeKey(getShimDataTypes()[0].toString()); + shimDataRequest.setNumToReturn(1l); + return shimDataRequest; + } + @Override public AuthorizationResponse handleAuthorizationResponse(HttpServletRequest servletRequest) throws ShimException { diff --git a/shim-server/src/main/java/org/openmhealth/shim/Shim.java b/shim-server/src/main/java/org/openmhealth/shim/Shim.java index 068443ba..d90bdb9b 100644 --- a/shim-server/src/main/java/org/openmhealth/shim/Shim.java +++ b/shim-server/src/main/java/org/openmhealth/shim/Shim.java @@ -1,11 +1,11 @@ /* - * Copyright 2014 Open mHealth + * 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 + * 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, @@ -36,18 +36,11 @@ public interface Shim { String getShimKey(); /** - * Secret code provided by the external data provider. + * Parameters (such as client ID and secret) provided by the external data provider. * - * @return - String value + * @return - the parameters for this shim */ - String getClientSecret(); - - /** - * Client id provided by the external data provider. - * - * @return - String value - */ - String getClientId(); + ApplicationAccessParameters findApplicationAccessParameters(); /** * Base of the URL to which the user will @@ -117,4 +110,12 @@ AuthorizationResponse handleAuthorizationResponse( * @return Generic object wrapper including timestamp, shim, and results */ ShimDataResponse getData(final ShimDataRequest shimDataRequest) throws ShimException; + + + /** + * Check if this shim is properly configured. + * + * @return true if this shim is properly configured. + */ + boolean isConfigured(); } diff --git a/shim-server/src/main/java/org/openmhealth/shim/ShimAuthentication.java b/shim-server/src/main/java/org/openmhealth/shim/ShimAuthentication.java index 97b49991..7a492ffb 100644 --- a/shim-server/src/main/java/org/openmhealth/shim/ShimAuthentication.java +++ b/shim-server/src/main/java/org/openmhealth/shim/ShimAuthentication.java @@ -1,11 +1,11 @@ /* - * Copyright 2014 Open mHealth + * 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 + * 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, diff --git a/shim-server/src/main/java/org/openmhealth/shim/ShimBase.java b/shim-server/src/main/java/org/openmhealth/shim/ShimBase.java new file mode 100644 index 00000000..be87a47b --- /dev/null +++ b/shim-server/src/main/java/org/openmhealth/shim/ShimBase.java @@ -0,0 +1,58 @@ +/* + * 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; + +/** + * Base class for shims. + * + * @author Eric Jain + */ +public abstract class ShimBase implements Shim { + + private String clientId; + + private String clientSecret; + + private final ApplicationAccessParametersRepo applicationParametersRepo; + + protected ShimBase(ApplicationAccessParametersRepo applicationParametersRepo) { + this.applicationParametersRepo = applicationParametersRepo; + } + + @Override + public ApplicationAccessParameters findApplicationAccessParameters() { + ApplicationAccessParameters parameters = applicationParametersRepo.findByShimKey(getShimKey()); + if (parameters == null) { + parameters = new ApplicationAccessParameters(getShimKey(), clientId, clientSecret); + } + return parameters; + } + + public void setClientId(String clientId) { + this.clientId = clientId; + } + + public void setClientSecret(String clientSecret) { + this.clientSecret = clientSecret; + } + + @Override + public boolean isConfigured() { + ApplicationAccessParameters parameters = findApplicationAccessParameters(); + return parameters.getClientId() != null && parameters.getClientSecret() != null; + } +} diff --git a/shim-server/src/main/java/org/openmhealth/shim/ShimConfig.java b/shim-server/src/main/java/org/openmhealth/shim/ShimConfig.java deleted file mode 100644 index 14a47b85..00000000 --- a/shim-server/src/main/java/org/openmhealth/shim/ShimConfig.java +++ /dev/null @@ -1,28 +0,0 @@ -/* - * Copyright 2014 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; - -/** - * @author Danilo Bonilla - */ -public interface ShimConfig { - - String getClientId(); - - String getClientSecret(); - -} diff --git a/shim-server/src/main/java/org/openmhealth/shim/ShimDataType.java b/shim-server/src/main/java/org/openmhealth/shim/ShimDataType.java index 96f83303..83b6830f 100644 --- a/shim-server/src/main/java/org/openmhealth/shim/ShimDataType.java +++ b/shim-server/src/main/java/org/openmhealth/shim/ShimDataType.java @@ -1,11 +1,11 @@ /* - * Copyright 2014 Open mHealth + * 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 + * 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, @@ -23,7 +23,9 @@ */ public interface ShimDataType { - JsonDeserializer getNormalizer(); + default JsonDeserializer getNormalizer() { + return null; + } String name(); } diff --git a/shim-server/src/main/java/org/openmhealth/shim/ShimRegistry.java b/shim-server/src/main/java/org/openmhealth/shim/ShimRegistry.java index 85244bf1..24df98aa 100644 --- a/shim-server/src/main/java/org/openmhealth/shim/ShimRegistry.java +++ b/shim-server/src/main/java/org/openmhealth/shim/ShimRegistry.java @@ -1,11 +1,11 @@ /* - * Copyright 2014 Open mHealth + * 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 + * 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, diff --git a/shim-server/src/main/java/org/openmhealth/shim/ShimRegistryImpl.java b/shim-server/src/main/java/org/openmhealth/shim/ShimRegistryImpl.java index ed7d732b..9f6345be 100644 --- a/shim-server/src/main/java/org/openmhealth/shim/ShimRegistryImpl.java +++ b/shim-server/src/main/java/org/openmhealth/shim/ShimRegistryImpl.java @@ -1,11 +1,11 @@ /* - * Copyright 2014 Open mHealth + * 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 + * 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, @@ -16,25 +16,13 @@ package org.openmhealth.shim; -import org.openmhealth.shim.fatsecret.FatsecretConfig; -import org.openmhealth.shim.fatsecret.FatsecretShim; -import org.openmhealth.shim.fitbit.FitbitConfig; -import org.openmhealth.shim.fitbit.FitbitShim; -import org.openmhealth.shim.healthvault.HealthvaultConfig; -import org.openmhealth.shim.healthvault.HealthvaultShim; -import org.openmhealth.shim.jawbone.JawboneConfig; -import org.openmhealth.shim.jawbone.JawboneShim; -import org.openmhealth.shim.runkeeper.RunkeeperConfig; -import org.openmhealth.shim.runkeeper.RunkeeperShim; -import org.openmhealth.shim.withings.WithingsConfig; -import org.openmhealth.shim.withings.WithingsShim; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; import java.util.ArrayList; -import java.util.Arrays; import java.util.LinkedHashMap; import java.util.List; +import java.util.Map; import static java.lang.String.format; @@ -54,64 +42,21 @@ public class ShimRegistryImpl implements ShimRegistry { private ShimServerConfig shimServerConfig; @Autowired - private FitbitConfig fitbitConfig; + private List shims; - @Autowired - private FatsecretConfig fatsecretConfig; - - @Autowired - private HealthvaultConfig healthvaultConfig; - - @Autowired - private JawboneConfig jawboneConfig; - - @Autowired - private RunkeeperConfig runkeeperConfig; - - @Autowired - private WithingsConfig withingsConfig; - - private LinkedHashMap registryMap; + private Map registryMap; public ShimRegistryImpl() { } public void init() { - registryMap = new LinkedHashMap<>(); - - if (jawboneConfig.getClientId() != null && jawboneConfig.getClientSecret() != null) { - registryMap.put(JawboneShim.SHIM_KEY, - new JawboneShim( - authParametersRepo, accessParametersRepo, shimServerConfig, jawboneConfig)); - } - - if (runkeeperConfig.getClientId() != null && runkeeperConfig.getClientSecret() != null) { - registryMap.put(RunkeeperShim.SHIM_KEY, - new RunkeeperShim( - authParametersRepo, accessParametersRepo, shimServerConfig, runkeeperConfig)); - } - - if (fatsecretConfig.getClientId() != null && fatsecretConfig.getClientSecret() != null) { - registryMap.put(FatsecretShim.SHIM_KEY, - new FatsecretShim(authParametersRepo, shimServerConfig, fatsecretConfig)); - } - - if (withingsConfig.getClientId() != null && withingsConfig.getClientSecret() != null) { - registryMap.put(WithingsShim.SHIM_KEY, - new WithingsShim( - authParametersRepo, shimServerConfig, withingsConfig)); - } - - if (fitbitConfig.getClientId() != null && fitbitConfig.getClientSecret() != null) { - registryMap.put(FitbitShim.SHIM_KEY, - new FitbitShim(authParametersRepo, shimServerConfig, fitbitConfig)); - } - - if (healthvaultConfig.getClientId() != null) { - registryMap.put(HealthvaultShim.SHIM_KEY, - new HealthvaultShim( - authParametersRepo, shimServerConfig, healthvaultConfig)); + Map registryMap = new LinkedHashMap<>(); + for (Shim shim : shims) { + if (shim.isConfigured()) { + registryMap.put(shim.getShimKey(), shim); + } } + this.registryMap = registryMap; } @Override @@ -129,14 +74,7 @@ public Shim getShim(String shimKey) { @Override public List getAvailableShims() { - return Arrays.asList( - new JawboneShim(null, null, null, jawboneConfig), - new FatsecretShim(null, null, fatsecretConfig), - new RunkeeperShim(null, null, null, runkeeperConfig), - new WithingsShim(null, null, withingsConfig), - new HealthvaultShim(null, null, healthvaultConfig), - new FitbitShim(null, null, fitbitConfig) - ); + return shims; } @Override diff --git a/shim-server/src/main/java/org/openmhealth/shim/ShimServerConfig.java b/shim-server/src/main/java/org/openmhealth/shim/ShimServerConfig.java index f1207e0e..d9c0bae0 100644 --- a/shim-server/src/main/java/org/openmhealth/shim/ShimServerConfig.java +++ b/shim-server/src/main/java/org/openmhealth/shim/ShimServerConfig.java @@ -1,11 +1,11 @@ /* - * Copyright 2014 Open mHealth + * 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 + * 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, diff --git a/shim-server/src/main/java/org/openmhealth/shim/fatsecret/FatsecretConfig.java b/shim-server/src/main/java/org/openmhealth/shim/fatsecret/FatsecretConfig.java deleted file mode 100644 index f57cb742..00000000 --- a/shim-server/src/main/java/org/openmhealth/shim/fatsecret/FatsecretConfig.java +++ /dev/null @@ -1,59 +0,0 @@ -/* - * Copyright 2014 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.fatsecret; - -import org.openmhealth.shim.ApplicationAccessParameters; -import org.openmhealth.shim.ApplicationAccessParametersRepo; -import org.openmhealth.shim.ShimConfig; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.context.properties.ConfigurationProperties; -import org.springframework.stereotype.Component; - -/** - * @author Danilo Bonilla - */ -@Component -@ConfigurationProperties(prefix = "openmhealth.shim.fatsecret") -public class FatsecretConfig implements ShimConfig { - - @Autowired - private ApplicationAccessParametersRepo applicationParametersRepo; - - private String clientId; - - private String clientSecret; - - public String getClientId() { - ApplicationAccessParameters parameters = - applicationParametersRepo.findByShimKey(FatsecretShim.SHIM_KEY); - return parameters != null ? parameters.getClientId() : clientId; - } - - public void setClientId(String clientId) { - this.clientId = clientId; - } - - public String getClientSecret() { - ApplicationAccessParameters parameters = - applicationParametersRepo.findByShimKey(FatsecretShim.SHIM_KEY); - return parameters != null ? parameters.getClientSecret() : clientSecret; - } - - public void setClientSecret(String clientSecret) { - this.clientSecret = clientSecret; - } -} diff --git a/shim-server/src/main/java/org/openmhealth/shim/fatsecret/FatsecretShim.java b/shim-server/src/main/java/org/openmhealth/shim/fatsecret/FatsecretShim.java.txt similarity index 87% rename from shim-server/src/main/java/org/openmhealth/shim/fatsecret/FatsecretShim.java rename to shim-server/src/main/java/org/openmhealth/shim/fatsecret/FatsecretShim.java.txt index 4994ff7d..af268bdd 100644 --- a/shim-server/src/main/java/org/openmhealth/shim/fatsecret/FatsecretShim.java +++ b/shim-server/src/main/java/org/openmhealth/shim/fatsecret/FatsecretShim.java.txt @@ -24,6 +24,9 @@ import org.joda.time.DateTime; import org.joda.time.MutableDateTime; import org.openmhealth.shim.*; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.stereotype.Component; import java.io.IOException; import java.net.URL; @@ -36,6 +39,8 @@ * * @author Danilo Bonilla */ +@Component +@ConfigurationProperties(prefix = "openmhealth.shim.fatsecret") public class FatsecretShim extends OAuth1ShimBase { public static final String SHIM_KEY = "fatsecret"; @@ -48,13 +53,11 @@ public class FatsecretShim extends OAuth1ShimBase { private static final String TOKEN_URL = "http://www.fatsecret.com/oauth/access_token"; - private FatsecretConfig config; - - public FatsecretShim(AuthorizationRequestParametersRepo authorizationRequestParametersRepo, - ShimServerConfig shimServerConfig, - FatsecretConfig fatsecretConfig) { - super(authorizationRequestParametersRepo, shimServerConfig); - this.config = fatsecretConfig; + @Autowired + public FatsecretShim(ApplicationAccessParametersRepo applicationParametersRepo, + AuthorizationRequestParametersRepo authorizationRequestParametersRepo, + ShimServerConfig shimServerConfig) { + super(applicationParametersRepo, authorizationRequestParametersRepo, shimServerConfig); } @Override @@ -72,16 +75,6 @@ public String getShimKey() { return SHIM_KEY; } - @Override - public String getClientSecret() { - return config.getClientSecret(); - } - - @Override - public String getClientId() { - return config.getClientId(); - } - @Override public String getBaseRequestTokenUrl() { return REQUEST_TOKEN_URL; diff --git a/shim-server/src/main/java/org/openmhealth/shim/fitbit/Fitbit.md b/shim-server/src/main/java/org/openmhealth/shim/fitbit/Fitbit.md new file mode 100644 index 00000000..7793166f --- /dev/null +++ b/shim-server/src/main/java/org/openmhealth/shim/fitbit/Fitbit.md @@ -0,0 +1,73 @@ +### still a rough draft + +# api +api url: https://api.fitbit.com + +## authentication + +### OAuth 1.0a +- protocol: OAuth 1.0a for production environments +- reference: https://dev.fitbit.com/docs/oauth1/ +- authorization URL: https://www.fitbit.com/oauth/authorize + -request token: https://www.api.fitbit.com/oauth/request_token +-access token: https://www.api.fitbit.com/oauth/access_token + +### OAuth 2.0 +-protocol: OAuth 2.0 currently in beta + +# pagination +- supported: Mostly no, however newer api calls (currently in beta) are adding support, such as activities list + +# rate limit +- public data and non-user authenticated data: 150 / hour +- per user: 150 / hr +- limit header: Fitbit-Rate-Limit-Limit +- remaining header: Fitbit-Rate-Limit-Remaining +- next reset time header: Fitbit-Rate-Limit-Remaining (seconds until reset) + +# time zone and time representation +- time zone information is not captured for dates and times; it is not possible to infer timezone through any means + +# endpoints + +## get user info +- Uri: https://api.fitbit.com//user//profile. (api version is currently 1) +- Reference: https://wiki.fitbit.com/display/API/API-Get-User-Info + +Returns information from the user’s profile in the form of a json object “user” with properties ranging from “city” and “dateOfBirth” to “gender” and “weight”, and “timezone”. Unfortunately the timezone is not related to the data points from other endpoints, so it should not be used to set the timezone for timestamps in data points. + +## get activities +- Uri: https://api.fitbit.com//user//activities/date/. (api version is currently 1) +- Reference: https://wiki.fitbit.com/display/API/API-Get-Activities +- Measures: step count, physical activity + +Returns all the activities for a user on a specified day and contains a list of activities, goals, and summary of the day. The request takes an optional request header of Accept-Language, which can be used to specify the measurement unit system for the values. By default, uses metric system. “activities” is a JSON array of activity JSON objects with details about each activity logged on that day. “goals” is a JSON object with activity-oriented goals - “caloriesOut”, “distance”, “floors”, “steps”. “summary” is a JSON object that summarizes key outcomes aggregated over all activities for that day - “activityCalories”, “activityOut”, “elevation”, etc, and an array “distances,” which breaks down the distances of various activities completed that day. + + +## get body weight +- Uris: +https://api.fitbit.com//user/-/body/log/weight/date/. (Where api version is currently 1) +https://api.fitbit.com//user/-/body/log/weight/date//. +https://api.fitbit.com//user/-/body/log/weight/date//. +- Reference: https://wiki.fitbit.com/display/API/API-Get-Body-Weight +- Measures: body weight, body mass index + +Returns the body weight entries for a user for the time frames specified in the request. The date formats are either (1) a single day with given format YYYY-mm-dd, (2) with format YYYY-mm-dd and as one of the enumeration [1d, 7d, 30d, 1w, 1m], or (3) with format YYYY-mm-dd and with format YYYY-mm-dd that contains a period no longer than 31 days. We use the base-date/end-date format when shimmer requests are made for more than one day and use the single date format when only a single day is requested. + +The weight entries are returned as, “weight”, an array of JSON objects, each corresponding to a body weight entry within the time period. Each has a date, time, logId, weight, and bmi property. + +The request takes an optional request header of Accept-Language, which can be used to specify the measurement unit system for the values. By default, uses metric system. + +## get sleep +- Uri: https://api.fitbit.com//user//sleep/date/. (Where api version is currently 1) +- Reference: https://wiki.fitbit.com/display/API/API-Get-Sleep +- Measure: sleep duration + +Sleep data points are retrieved in requests for the date in which they ended. If an individual went to bed at 9:00p on 8/22/2015 and woke up at 8:00a on 8/23/2015 (in the same time zone), then this datapoint would be returned in the request for data on 2015-08-23, but NOT on the request for data on 2015-08-22. + +Returns the sleep log entries and summary for a user for a single day, specified in the request as a date in the format YYYY-mm-dd. The response content contains a JSON array, “sleep”, that contains objects related to sleep diary entries. Each entry has a “startTime” property for the time and date (in the format YYYY-mm-ddThh:mm:ss.nnn), “minutesAsleep” for that entry, “isMainSleep” which is a boolean stating whether its the main period of sleep for an individual, as well as a JSON array called “minuteData” that contains a JSON object for each minute the individual is asleep during that entry, with each object having two properties, “dateTime”, which is just a time (in the format hh:mm:ss) starting from the “startTime” date-time, and a “value” property which contains an integer from the enumeration from the following array ("1", "2", "3"), where "1" = "asleep", "2" = "awake", "3" = "really awake". + +An important consideration around sleep is that the data can be connected or unconnected and associated with a specific day. The key is to reference the startTime property of each sleep log to identify the date and time that the entry was created. The objects within the “minuteData” array then are times that immediately follow the “startTime”. So although they have no date associated, the date can be inferred from the “startTime”. If the individual starts sleeping on day 1 at 23:30, then the first “minuteData” entry with the “dateTime” of 23:30:00, will be from the date of day 1. However a few hours later, the “minuteData” entry with the “dateTime” 02:30:00 will actually be for the following day, so would have the date of the next day. + +# issues +The Fitbit API does not provide time zone information for any data points, making it impossible to determine the instant that events took place or data points were recorded. We currently use UTC as the time zone for all data, even data that would not have occured in that time zone, because we have no other information to work with. diff --git a/shim-server/src/main/java/org/openmhealth/shim/fitbit/FitbitConfig.java b/shim-server/src/main/java/org/openmhealth/shim/fitbit/FitbitConfig.java deleted file mode 100644 index a4dc595c..00000000 --- a/shim-server/src/main/java/org/openmhealth/shim/fitbit/FitbitConfig.java +++ /dev/null @@ -1,59 +0,0 @@ -/* - * Copyright 2014 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; - -import org.openmhealth.shim.ApplicationAccessParameters; -import org.openmhealth.shim.ApplicationAccessParametersRepo; -import org.openmhealth.shim.ShimConfig; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.context.properties.ConfigurationProperties; -import org.springframework.stereotype.Component; - -/** - * @author Danilo Bonilla - */ -@Component -@ConfigurationProperties(prefix = "openmhealth.shim.fitbit") -public class FitbitConfig implements ShimConfig { - - private String clientId; - - private String clientSecret; - - @Autowired - private ApplicationAccessParametersRepo applicationParametersRepo; - - public String getClientId() { - ApplicationAccessParameters parameters = - applicationParametersRepo.findByShimKey(FitbitShim.SHIM_KEY); - return parameters != null ? parameters.getClientId() : clientId; - } - - public void setClientId(String clientId) { - this.clientId = clientId; - } - - public String getClientSecret() { - ApplicationAccessParameters parameters = - applicationParametersRepo.findByShimKey(FitbitShim.SHIM_KEY); - return parameters != null ? parameters.getClientSecret() : clientSecret; - } - - public void setClientSecret(String clientSecret) { - this.clientSecret = clientSecret; - } -} diff --git a/shim-server/src/main/java/org/openmhealth/shim/fitbit/FitbitShim.java b/shim-server/src/main/java/org/openmhealth/shim/fitbit/FitbitShim.java index 5f43e803..df095a22 100644 --- a/shim-server/src/main/java/org/openmhealth/shim/fitbit/FitbitShim.java +++ b/shim-server/src/main/java/org/openmhealth/shim/fitbit/FitbitShim.java @@ -1,11 +1,11 @@ /* - * Copyright 2014 Open mHealth + * 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 + * 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, @@ -16,42 +16,39 @@ package org.openmhealth.shim.fitbit; -import com.fasterxml.jackson.core.JsonParser; -import com.fasterxml.jackson.core.JsonProcessingException; -import com.fasterxml.jackson.databind.DeserializationContext; -import com.fasterxml.jackson.databind.JsonDeserializer; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; -import com.fasterxml.jackson.databind.module.SimpleModule; -import com.fasterxml.jackson.databind.node.ArrayNode; -import com.jayway.jsonpath.JsonPath; -import net.minidev.json.JSONArray; -import net.minidev.json.JSONObject; +import com.google.common.collect.Lists; import org.apache.commons.io.IOUtils; import org.apache.http.HttpEntity; import org.apache.http.HttpResponse; import org.apache.http.client.methods.HttpRequestBase; -import org.joda.time.DateTime; -import org.joda.time.format.DateTimeFormat; -import org.joda.time.format.DateTimeFormatter; -import org.openmhealth.schema.pojos.*; -import org.openmhealth.schema.pojos.build.*; -import org.openmhealth.schema.pojos.generic.MassUnitValue; +import org.openmhealth.schema.domain.omh.DataPoint; import org.openmhealth.shim.*; +import org.openmhealth.shim.fitbit.mapper.*; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.http.HttpMethod; +import org.springframework.stereotype.Component; import org.springframework.util.CollectionUtils; +import org.springframework.web.util.UriComponentsBuilder; import java.io.IOException; import java.io.StringWriter; -import java.math.BigDecimal; -import java.util.*; +import java.time.LocalDate; +import java.time.OffsetDateTime; +import java.time.ZoneId; +import java.util.ArrayList; +import java.util.List; + +import static java.util.Collections.singletonList; -import static org.openmhealth.schema.pojos.generic.DurationUnitValue.*; -import static org.openmhealth.schema.pojos.generic.LengthUnitValue.LengthUnit.*; /** * @author Danilo Bonilla */ +@Component +@ConfigurationProperties(prefix = "openmhealth.shim.fitbit") public class FitbitShim extends OAuth1ShimBase { public static final String SHIM_KEY = "fitbit"; @@ -64,13 +61,12 @@ public class FitbitShim extends OAuth1ShimBase { private static final String TOKEN_URL = "https://api.fitbit.com/oauth/access_token"; - private FitbitConfig config; + @Autowired + public FitbitShim(ApplicationAccessParametersRepo applicationParametersRepo, + AuthorizationRequestParametersRepo authorizationRequestParametersRepo, + ShimServerConfig shimServerConfig) { - public FitbitShim(AuthorizationRequestParametersRepo authorizationRequestParametersRepo, - ShimServerConfig shimServerConfig, - FitbitConfig fitbitConfig) { - super(authorizationRequestParametersRepo, shimServerConfig); - this.config = fitbitConfig; + super(applicationParametersRepo, authorizationRequestParametersRepo, shimServerConfig); } @Override @@ -88,16 +84,6 @@ public String getShimKey() { return SHIM_KEY; } - @Override - public String getClientSecret() { - return config.getClientSecret(); - } - - @Override - public String getClientId() { - return config.getClientId(); - } - @Override public String getBaseRequestTokenUrl() { return REQUEST_TOKEN_URL; @@ -121,11 +107,6 @@ protected HttpMethod getAccessTokenMethod() { return HttpMethod.POST; } - private static DateTimeFormatter formatterMins = - DateTimeFormat.forPattern("yyyy-MM-dd HH:mm"); - - private static final DateTimeFormatter dayFormatter = DateTimeFormat.forPattern("yyyy-MM-dd"); - @Override public ShimDataType[] getShimDataTypes() { return FitbitDataType.values(); @@ -133,307 +114,16 @@ public ShimDataType[] getShimDataTypes() { public enum FitbitDataType implements ShimDataType { - WEIGHT( - "body/log/weight", - new JsonDeserializer() { - @Override - public ShimDataResponse deserialize(JsonParser jsonParser, - DeserializationContext ctxt) - throws IOException, JsonProcessingException { - JsonNode responseNode = jsonParser.getCodec().readTree(jsonParser); - String rawJson = responseNode.toString(); - - List bodyWeights = new ArrayList<>(); - //JsonPath bodyWeightsPath = JsonPath.compile("$result.content.weight[*]"); - JsonPath bodyWeightsPath = JsonPath.compile("$weight[*]"); - - List fbWeights = JsonPath.read(rawJson, bodyWeightsPath.getPath()); - if (CollectionUtils.isEmpty(fbWeights)) { - return ShimDataResponse.result(FitbitShim.SHIM_KEY, null); - } - ObjectMapper mapper = new ObjectMapper(); - for (Object fva : fbWeights) { - JsonNode fbWeight = mapper.readTree(((JSONObject) fva).toJSONString()); - - String dateStr = fbWeight.get("date").asText(); - dateStr += fbWeight.get("time") != null ? "T" + fbWeight.get("time").asText() : ""; - - DateTime dateTimeWhen = new DateTime(dateStr); - BodyWeight bodyWeight = new BodyWeightBuilder() - .setWeight( - fbWeight.get("weight").asText(), - MassUnitValue.MassUnit.kg.toString()) - .setTimeTaken(dateTimeWhen).build(); - - bodyWeights.add(bodyWeight); - } - Map results = new HashMap<>(); - results.put(BodyWeight.SCHEMA_BODY_WEIGHT, bodyWeights); - return ShimDataResponse.result(FitbitShim.SHIM_KEY, results); - } - } - ), - - HEART( - "heart", - new JsonDeserializer() { - @Override - public ShimDataResponse deserialize(JsonParser jsonParser, - DeserializationContext ctxt) - throws IOException { - JsonNode responseNode = jsonParser.getCodec().readTree(jsonParser); - String rawJson = responseNode.toString(); - - List heartRates = new ArrayList<>(); - JsonPath heartPath = JsonPath.compile("$.result.content.heart[*]"); - - String dateString = JsonPath.read(rawJson, "$.result.date").toString(); - - List fbHearts = JsonPath.read(rawJson, heartPath.getPath()); - if (CollectionUtils.isEmpty(fbHearts)) { - return ShimDataResponse.result(FitbitShim.SHIM_KEY, null); - } - - ObjectMapper mapper = new ObjectMapper(); - for (Object fva : fbHearts) { - JsonNode fbHeart = mapper.readTree(((JSONObject) fva).toJSONString()); - - String heartDate = dateString; - if (fbHeart.get("time") != null) { - heartDate += "T" + fbHeart.get("time").asText(); - heartRates.add(new HeartRateBuilder() - .withRate(fbHeart.get("heartRate").asInt()) - .withTimeTaken(new DateTime(heartDate)).build()); - } - } - Map results = new HashMap<>(); - results.put(HeartRate.SCHEMA_HEART_RATE, heartRates); - return ShimDataResponse.result(FitbitShim.SHIM_KEY, results); - } - } - ), - - BLOOD_PRESSURE( - "bp", - new JsonDeserializer() { - @Override - public ShimDataResponse deserialize(JsonParser jsonParser, - DeserializationContext ctxt) - throws IOException, JsonProcessingException { - JsonNode responseNode = jsonParser.getCodec().readTree(jsonParser); - String rawJson = responseNode.toString(); - - List bloodPressures = new ArrayList<>(); - JsonPath bpPath = JsonPath.compile("$.result.content.bp[*]"); - - String dateString = JsonPath.read(rawJson, "$.result.date").toString(); - - List fbBloodPressures = JsonPath.read(rawJson, bpPath.getPath()); - if (CollectionUtils.isEmpty(fbBloodPressures)) { - return ShimDataResponse.result(FitbitShim.SHIM_KEY, null); - } - - ObjectMapper mapper = new ObjectMapper(); - for (Object fva : fbBloodPressures) { - JsonNode fbBp = mapper.readTree(((JSONObject) fva).toJSONString()); - - String bpDate = dateString; - if (fbBp.get("time") != null) { - bpDate += "T" + fbBp.get("time").asText(); - } - - bloodPressures.add(new BloodPressureBuilder() - .setTimeTaken(new DateTime(bpDate)) - .setValues( - new BigDecimal(fbBp.get("systolic").asText()), - new BigDecimal(fbBp.get("diastolic").asText()) - ).build()); - } - Map results = new HashMap<>(); - results.put(BloodPressure.SCHEMA_BLOOD_PRESSURE, bloodPressures); - return ShimDataResponse.result(FitbitShim.SHIM_KEY, results); - } - } - ), - - BLOOD_GLUCOSE( - "glucose", - new JsonDeserializer() { - @Override - public ShimDataResponse deserialize(JsonParser jsonParser, - DeserializationContext ctxt) - throws IOException, JsonProcessingException { - JsonNode responseNode = jsonParser.getCodec().readTree(jsonParser); - String rawJson = responseNode.toString(); - - List bloodGlucoses = new ArrayList<>(); - JsonPath bpPath = JsonPath.compile("$.result.content.glucose[*]"); - - String dateString = JsonPath.read(rawJson, "$.result.date").toString(); - - List fbBloodPressures = JsonPath.read(rawJson, bpPath.getPath()); - if (CollectionUtils.isEmpty(fbBloodPressures)) { - return ShimDataResponse.result(FitbitShim.SHIM_KEY, null); - } - - ObjectMapper mapper = new ObjectMapper(); - for (Object fva : fbBloodPressures) { - JsonNode fbBp = mapper.readTree(((JSONObject) fva).toJSONString()); - - String bpDate = dateString; - if (fbBp.get("time") != null) { - bpDate += "T" + fbBp.get("time").asText(); - } - - bloodGlucoses.add(new BloodGlucoseBuilder() - .setTimeTaken(new DateTime(bpDate)) - .setMgdLValue(new BigDecimal(fbBp.get("glucose").asText())).build()); - } - Map results = new HashMap<>(); - results.put(BloodGlucose.SCHEMA_BLOOD_GLUCOSE, bloodGlucoses); - return ShimDataResponse.result(FitbitShim.SHIM_KEY, results); - } - } - ), - - STEPS("activities/steps", new JsonDeserializer() { - @Override - public ShimDataResponse deserialize(JsonParser jsonParser, - DeserializationContext deserializationContext) - throws IOException { - - JsonNode responseNode = jsonParser.getCodec().readTree(jsonParser); - String rawJson = responseNode.toString(); - - List steps = new ArrayList<>(); - JsonPath stepsPath = JsonPath.compile("$.[*].result.content[*]"); - - Object oneMinStepEntries = JsonPath.read(rawJson, stepsPath.getPath()); - - if (oneMinStepEntries == null) { - return ShimDataResponse.empty(FitbitShim.SHIM_KEY); - } - - DateTimeFormatter formatter = DateTimeFormat.forPattern("yyyy-MM-dd HH:mm:ss"); - ObjectMapper mapper = new ObjectMapper(); - - /** - * Determine if many items were returned or just one - * and cast appropriately. - */ - ArrayNode nodes; - String jsonString; - if (oneMinStepEntries instanceof JSONArray) { - jsonString = ((JSONArray) oneMinStepEntries).toJSONString(); - } else { - jsonString = "[" + - ((JSONObject) oneMinStepEntries).toJSONString() + "]"; - } - nodes = (ArrayNode) mapper.readTree(jsonString); - - for (Object node1 : nodes) { - JsonNode fbStepNode = (JsonNode) node1; - - /** - * The value 'activities-steps-intraday' will only be available - * to apps that have partner level access to fitbit. - * - * https://wiki.fitbit.com/display/API/Fitbit+Partner+API - * - * Apps without this access will only get one step entry per day as a normalized - * response. The minute level resolutions will be retrieved and parsed - * only for partner level apps. - */ - String dateString = - (fbStepNode.get("activities-steps")).get(0).get("dateTime").asText(); - if (fbStepNode.get("activities-steps-intraday") != null) { - ArrayNode dataset = (ArrayNode) - fbStepNode.get("activities-steps-intraday").get("dataset"); - for (JsonNode stepMinute : dataset) { - if (stepMinute.get("value").asInt() > 0) { - steps.add(new StepCountBuilder() - .withStartAndDuration( - formatter.parseDateTime( - dateString + " " + stepMinute.get("time").asText()), - 1d, DurationUnit.min - ).setSteps(stepMinute.get("value").asInt()) - .build()); - } - } - } else { - /** - * One entry is created for the whole day. No finer resolution is available. - */ - String dayString = dateString + " 00:00:00"; - Integer daySteps = (fbStepNode.get("activities-steps")).get(0).get("value").asInt(); - steps.add(new StepCountBuilder().withStartAndDuration( - formatter.parseDateTime(dayString), 1d, DurationUnit.d - ).setSteps(daySteps).build()); - } - } - Map results = new HashMap<>(); - results.put(StepCount.SCHEMA_STEP_COUNT, steps); - return ShimDataResponse.result(FitbitShim.SHIM_KEY, results); - } - }), - - ACTIVITY( - "activities", - new JsonDeserializer() { - @Override - public ShimDataResponse deserialize(JsonParser jsonParser, - DeserializationContext deserializationContext) - throws IOException { - JsonNode responseNode = jsonParser.getCodec().readTree(jsonParser); - String rawJson = responseNode.toString(); - - List activities = new ArrayList<>(); - - JsonPath activityPath = JsonPath.compile("$.result.content.activities[*]"); - - final List fitbitActivities = JsonPath.read(rawJson, activityPath.getPath()); - if (CollectionUtils.isEmpty(fitbitActivities)) { - return ShimDataResponse.result(FitbitShim.SHIM_KEY, null); - } - ObjectMapper mapper = new ObjectMapper(); - for (Object fva : fitbitActivities) { - final JsonNode fitbitActivity = mapper.readTree(((JSONObject) fva).toJSONString()); - - String dateString = fitbitActivity.get("startDate").asText() - + (fitbitActivity.get("startTime") != null ? - " " + fitbitActivity.get("startTime").asText() : ""); - - DateTime startTime = formatterMins.parseDateTime(dateString); - - Activity activity = new ActivityBuilder() - .setActivityName(fitbitActivity.get("activityParentName").asText()) - .setDistance(fitbitActivity.get("distance").asDouble(), m) - .withStartAndDuration( - startTime, fitbitActivity.get("duration").asDouble(), DurationUnit.ms) - .build(); - - activities.add(activity); - } - - Map results = new HashMap<>(); - results.put(Activity.SCHEMA_ACTIVITY, activities); - return ShimDataResponse.result(FitbitShim.SHIM_KEY, results); - } - } - ); + WEIGHT("body/log/weight"), + SLEEP("sleep"), + BODY_MASS_INDEX("body/log/weight"), + STEPS("activities/steps"), + ACTIVITY("activities"); private String endPoint; - private JsonDeserializer normalizer; - - FitbitDataType(String endPoint, JsonDeserializer normalizer) { + FitbitDataType(String endPoint) { this.endPoint = endPoint; - this.normalizer = normalizer; - } - - @Override - public JsonDeserializer getNormalizer() { - return normalizer; } public String getEndPoint() { @@ -443,6 +133,7 @@ public String getEndPoint() { @Override public ShimDataResponse getData(ShimDataRequest shimDataRequest) throws ShimException { + AccessParameters accessParameters = shimDataRequest.getAccessParameters(); String accessToken = accessParameters.getAccessToken(); String tokenSecret = accessParameters.getTokenSecret(); @@ -450,45 +141,51 @@ public ShimDataResponse getData(ShimDataRequest shimDataRequest) throws ShimExce FitbitDataType fitbitDataType; try { fitbitDataType = FitbitDataType.valueOf(shimDataRequest.getDataTypeKey().trim().toUpperCase()); - } catch (NullPointerException | IllegalArgumentException e) { + } + catch (NullPointerException | IllegalArgumentException e) { throw new ShimException("Null or Invalid data type parameter: " - + shimDataRequest.getDataTypeKey() - + " in shimDataRequest, cannot retrieve data."); + + shimDataRequest.getDataTypeKey() + + " in shimDataRequest, cannot retrieve data."); } /*** * Setup default date parameters */ - DateTime today = - dayFormatter.parseDateTime(new DateTime().toString(dayFormatter)); //ensure beginning of today + OffsetDateTime today = LocalDate.now().atStartOfDay(ZoneId.of("Z")).toOffsetDateTime(); - DateTime startDate = shimDataRequest.getStartDate() == null ? - today.minusDays(1) : shimDataRequest.getStartDate(); + OffsetDateTime startDate = shimDataRequest.getStartDateTime() == null ? + today.minusDays(1) : shimDataRequest.getStartDateTime(); - DateTime endDate = shimDataRequest.getEndDate() == null ? - today.plusDays(1) : shimDataRequest.getEndDate(); + OffsetDateTime endDate = shimDataRequest.getEndDateTime() == null ? + today.plusDays(1) : shimDataRequest.getEndDateTime(); - DateTime currentDate = startDate; + OffsetDateTime currentDate = startDate; if (fitbitDataType.equals(FitbitDataType.WEIGHT)) { return getRangeData( - startDate, endDate, fitbitDataType, - shimDataRequest.getNormalize(), accessToken, tokenSecret); - } else { + startDate, endDate, fitbitDataType, + shimDataRequest.getNormalize(), accessToken, tokenSecret); + } + else { /** * Fitbit's API limits you to making a request for each given day * of data. Thus we make a request for each day in the submitted time * range and then aggregate the response based on the normalization parameter. */ List dayResponses = new ArrayList<>(); - while (currentDate.toDate().before(endDate.toDate()) || - currentDate.toDate().equals(endDate.toDate())) { + + while (currentDate.toLocalDate().isBefore(endDate.toLocalDate()) || + currentDate.toLocalDate().isEqual(endDate.toLocalDate())) { + dayResponses.add(getDaysData(currentDate, fitbitDataType, - shimDataRequest.getNormalize(), accessToken, tokenSecret)); + shimDataRequest.getNormalize(), accessToken, tokenSecret)); currentDate = currentDate.plusDays(1); } - return shimDataRequest.getNormalize() ? - aggregateNormalized(dayResponses) : aggregateIntoList(dayResponses); + + ShimDataResponse shimDataResponse = shimDataRequest.getNormalize() ? + aggregateNormalized(dayResponses) : aggregateIntoList(dayResponses); + + return shimDataResponse; } } @@ -499,25 +196,27 @@ public ShimDataResponse getData(ShimDataRequest shimDataRequest) throws ShimExce */ @SuppressWarnings("unchecked") private ShimDataResponse aggregateNormalized(List dayResponses) { + if (CollectionUtils.isEmpty(dayResponses)) { return ShimDataResponse.empty(FitbitShim.SHIM_KEY); } - Map> aggregateMap = new HashMap<>(); + + List aggregateDataPoints = Lists.newArrayList(); + for (ShimDataResponse dayResponse : dayResponses) { if (dayResponse.getBody() != null) { - Map> dayMap = - (Map>) dayResponse.getBody(); - for (String typeKey : dayMap.keySet()) { - if (!aggregateMap.containsKey(typeKey)) { - aggregateMap.put(typeKey, new ArrayList<>()); - } - aggregateMap.get(typeKey).addAll(dayMap.get(typeKey)); + + List dayList = (List) dayResponse.getBody(); + + for (DataPoint dataPoint : dayList) { + + aggregateDataPoints.add(dataPoint); } + } } - return aggregateMap.size() == 0 ? - ShimDataResponse.empty(FitbitShim.SHIM_KEY) : - ShimDataResponse.result(FitbitShim.SHIM_KEY, aggregateMap); + + return ShimDataResponse.result(FitbitShim.SHIM_KEY, aggregateDataPoints); } /** @@ -537,19 +236,21 @@ private ShimDataResponse aggregateIntoList(List dayResponses) } } return responses.size() == 0 ? ShimDataResponse.empty(FitbitShim.SHIM_KEY) : - ShimDataResponse.result(FitbitShim.SHIM_KEY, responses); + ShimDataResponse.result(FitbitShim.SHIM_KEY, responses); } private ShimDataResponse executeRequest(String endPointUrl, - String accessToken, - String tokenSecret, - boolean normalize, - FitbitDataType fitbitDataType + String accessToken, + String tokenSecret, + boolean normalize, + FitbitDataType fitbitDataType ) throws ShimException { + ApplicationAccessParameters parameters = findApplicationAccessParameters(); HttpRequestBase dataRequest = - OAuth1Utils.getSignedRequest(HttpMethod.GET, - endPointUrl, getClientId(), getClientSecret(), accessToken, tokenSecret, null); + OAuth1Utils.getSignedRequest(HttpMethod.GET, + endPointUrl, parameters.getClientId(), parameters.getClientSecret(), accessToken, tokenSecret, + null); HttpResponse response; try { @@ -563,84 +264,144 @@ private ShimDataResponse executeRequest(String endPointUrl, ObjectMapper objectMapper = new ObjectMapper(); if (normalize) { - SimpleModule module = new SimpleModule(); - module.addDeserializer(ShimDataResponse.class, fitbitDataType.getNormalizer()); - objectMapper.registerModule(module); - return objectMapper.readValue(jsonContent, ShimDataResponse.class); - } else { + + IOUtils.copy(responseEntity.getContent(), writer); + + JsonNode jsonNode = objectMapper.readValue(writer.toString(), JsonNode.class); + + FitbitDataPointMapper dataPointMapper; + + switch ( fitbitDataType ) { + case STEPS: + dataPointMapper = new FitbitStepCountDataPointMapper(); + break; + case ACTIVITY: + dataPointMapper = new FitbitPhysicalActivityDataPointMapper(); + break; + case WEIGHT: + dataPointMapper = new FitbitBodyWeightDataPointMapper(); + break; + case SLEEP: + dataPointMapper = new FitbitSleepDurationDataPointMapper(); + break; + case BODY_MASS_INDEX: + dataPointMapper = new FitbitBodyMassIndexDataPointMapper(); + break; + default: + throw new UnsupportedOperationException(); + } + + return ShimDataResponse + .result(FitbitShim.SHIM_KEY, dataPointMapper.asDataPoints(singletonList(jsonNode))); + + } + else { + return ShimDataResponse.result(FitbitShim.SHIM_KEY, - objectMapper.readTree(jsonContent)); + objectMapper.readTree(jsonContent)); } - } catch (IOException e) { + } + catch (IOException e) { throw new ShimException("Could not fetch data", e); - } finally { + } + finally { dataRequest.releaseConnection(); } } - private ShimDataResponse getRangeData(DateTime fromTime, - DateTime toTime, - FitbitDataType fitbitDataType, - boolean normalize, - String accessToken, String tokenSecret) throws ShimException { + private ShimDataResponse getRangeData(OffsetDateTime fromTime, + OffsetDateTime toTime, + FitbitDataType fitbitDataType, + boolean normalize, + String accessToken, String tokenSecret) throws ShimException { - String fromDateString = fromTime.toString(dayFormatter); - String toDateString = toTime.toString(dayFormatter); + String fromDateString = fromTime.toLocalDate().toString(); + String toDateString = toTime.toLocalDate().toString(); - String endPointUrl = DATA_URL; - endPointUrl += "/1/user/-/" - + fitbitDataType.getEndPoint() + "/date/" + fromDateString + "/" + toDateString - + (fitbitDataType == FitbitDataType.STEPS ? "/1d/1min" : "") //special setting for time series - + ".json"; + UriComponentsBuilder uriComponentsBuilder = UriComponentsBuilder.fromUriString(DATA_URL). + path("/1/user/-/{fitbitDataTypeEndpoint}/date/{fromDateString}/{toDateString}{stepTimeSeries}.json"); + String endpointUrl = + uriComponentsBuilder.buildAndExpand(fitbitDataType.getEndPoint(), fromDateString, toDateString, + (fitbitDataType == FitbitDataType.STEPS ? "/1d/1min" : "")).encode().toUriString(); - return executeRequest(endPointUrl, accessToken, tokenSecret, normalize, fitbitDataType); + return executeRequest(endpointUrl, accessToken, tokenSecret, normalize, fitbitDataType); } - private ShimDataResponse getDaysData(DateTime dateTime, - FitbitDataType fitbitDataType, - boolean normalize, - String accessToken, String tokenSecret) throws ShimException { + private ShimDataResponse getDaysData(OffsetDateTime dateTime, + FitbitDataType fitbitDataType, + boolean normalize, + String accessToken, String tokenSecret) throws ShimException { - String dateString = dateTime.toString(dayFormatter); + String dateString = dateTime.toLocalDate().toString(); - String endPointUrl = DATA_URL; - endPointUrl += "/1/user/-/" - + fitbitDataType.getEndPoint() + "/date/" + dateString - + (fitbitDataType == FitbitDataType.STEPS ? "/1d/1min" : "") //special setting for time series - + ".json"; + UriComponentsBuilder uriComponentsBuilder = UriComponentsBuilder.fromUriString(DATA_URL). + path("/1/user/-/{fitbitDataTypeEndpoint}/date/{dateString}{stepTimeSeries}.json"); + String endpointUrl = uriComponentsBuilder.buildAndExpand(fitbitDataType.getEndPoint(), dateString, + (fitbitDataType == FitbitDataType.STEPS ? "/1d/1min" : "")).encode().toString(); + ApplicationAccessParameters parameters = findApplicationAccessParameters(); HttpRequestBase dataRequest = - OAuth1Utils.getSignedRequest(HttpMethod.GET, - endPointUrl, getClientId(), getClientSecret(), accessToken, tokenSecret, null); + OAuth1Utils.getSignedRequest(HttpMethod.GET, + endpointUrl, parameters.getClientId(), parameters.getClientSecret(), accessToken, tokenSecret, + null); HttpResponse response; try { response = httpClient.execute(dataRequest); HttpEntity responseEntity = response.getEntity(); - /** - * The fitbit API's system works by retrieving each day's - * data. The date captured is not returned in the data from fitbit because - * it's implicit so we create a JSON wrapper that includes it. - */ StringWriter writer = new StringWriter(); IOUtils.copy(responseEntity.getContent(), writer); - String jsonContent = "{\"result\": {\"date\": \"" + dateString + "\" " + - ",\"content\": " + writer.toString() + "}}"; ObjectMapper objectMapper = new ObjectMapper(); + if (normalize) { - SimpleModule module = new SimpleModule(); - module.addDeserializer(ShimDataResponse.class, fitbitDataType.getNormalizer()); - objectMapper.registerModule(module); - return objectMapper.readValue(jsonContent, ShimDataResponse.class); - } else { + + JsonNode jsonNode = objectMapper.readValue(writer.toString(), JsonNode.class); + + FitbitDataPointMapper dataPointMapper; + + switch ( fitbitDataType ) { + case STEPS: + dataPointMapper = new FitbitStepCountDataPointMapper(); + break; + case ACTIVITY: + dataPointMapper = new FitbitPhysicalActivityDataPointMapper(); + break; + case WEIGHT: + dataPointMapper = new FitbitBodyWeightDataPointMapper(); + break; + case SLEEP: + dataPointMapper = new FitbitSleepDurationDataPointMapper(); + break; + case BODY_MASS_INDEX: + dataPointMapper = new FitbitBodyMassIndexDataPointMapper(); + break; + default: + throw new UnsupportedOperationException(); + } + + return ShimDataResponse + .result(FitbitShim.SHIM_KEY, dataPointMapper.asDataPoints(singletonList(jsonNode))); + + } + else { + /** + * The fitbit API's system works by retrieving each day's + * data. The date captured is not returned in the data from fitbit because + * it's implicit so we create a JSON wrapper that includes it. + */ + String jsonContent = "{\"result\": {\"date\": \"" + dateString + "\" " + + ",\"content\": " + writer.toString() + "}}"; + return ShimDataResponse.result(FitbitShim.SHIM_KEY, - objectMapper.readTree(jsonContent)); + objectMapper.readTree(jsonContent)); } - } catch (IOException e) { + } + catch (IOException e) { throw new ShimException("Could not fetch data", e); - } finally { + } + finally { dataRequest.releaseConnection(); } } diff --git a/shim-server/src/main/java/org/openmhealth/shim/fitbit/mapper/FitbitBodyMassIndexDataPointMapper.java b/shim-server/src/main/java/org/openmhealth/shim/fitbit/mapper/FitbitBodyMassIndexDataPointMapper.java new file mode 100644 index 00000000..96e90802 --- /dev/null +++ b/shim-server/src/main/java/org/openmhealth/shim/fitbit/mapper/FitbitBodyMassIndexDataPointMapper.java @@ -0,0 +1,72 @@ +/* + * 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.fitbit.mapper; + +import com.fasterxml.jackson.databind.JsonNode; +import org.openmhealth.schema.domain.omh.BodyMassIndex; +import org.openmhealth.schema.domain.omh.BodyMassIndexUnit; +import org.openmhealth.schema.domain.omh.DataPoint; +import org.openmhealth.schema.domain.omh.TypedUnitValue; + +import java.time.OffsetDateTime; +import java.util.Optional; + +import static org.openmhealth.shim.common.mapper.JsonNodeMappingSupport.asOptionalLong; +import static org.openmhealth.shim.common.mapper.JsonNodeMappingSupport.asRequiredDouble; + + +/** + * A mapper from Fitbit Resource API body/log/weight responses to {@link BodyMassIndex} objects + * + * @author Chris Schaefbauer + */ +public class FitbitBodyMassIndexDataPointMapper extends FitbitDataPointMapper { + + /** + * Maps a JSON response node from the Fitbit API into a {@link BodyMassIndex} measure + * + * @param node a JSON node for an individual object in the "weight" array retrieved from the body/log/weight Fitbit + * API call + * @return a {@link DataPoint} object containing a {@link BodyMassIndex} measure with the appropriate values from + * the JSON node parameter, wrapped as an {@link Optional} + */ + @Override + protected Optional> asDataPoint(JsonNode node) { + + TypedUnitValue bmiValue = + new TypedUnitValue(BodyMassIndexUnit.KILOGRAMS_PER_SQUARE_METER, + asRequiredDouble(node, "bmi")); + BodyMassIndex.Builder builder = new BodyMassIndex.Builder(bmiValue); + + Optional dateTime = combineDateTimeAndTimezone(node); + + if ( dateTime.isPresent()) { + builder.setEffectiveTimeFrame(dateTime.get()); + } + + Optional externalId = asOptionalLong(node, "logId"); + return Optional.of(newDataPoint(builder.build(), externalId.orElse(null))); + } + + /** + * @return the name of the list node returned from Fitbit Resource API body/log/weight response + */ + @Override + protected String getListNodeName() { + return "weight"; + } +} diff --git a/shim-server/src/main/java/org/openmhealth/shim/fitbit/mapper/FitbitBodyWeightDataPointMapper.java b/shim-server/src/main/java/org/openmhealth/shim/fitbit/mapper/FitbitBodyWeightDataPointMapper.java new file mode 100644 index 00000000..c02ca42d --- /dev/null +++ b/shim-server/src/main/java/org/openmhealth/shim/fitbit/mapper/FitbitBodyWeightDataPointMapper.java @@ -0,0 +1,73 @@ +/* + * 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.fitbit.mapper; + +import com.fasterxml.jackson.databind.JsonNode; +import org.openmhealth.schema.domain.omh.BodyWeight; +import org.openmhealth.schema.domain.omh.DataPoint; +import org.openmhealth.schema.domain.omh.MassUnit; +import org.openmhealth.schema.domain.omh.MassUnitValue; + +import java.time.OffsetDateTime; +import java.util.Optional; + +import static org.openmhealth.shim.common.mapper.JsonNodeMappingSupport.asOptionalLong; +import static org.openmhealth.shim.common.mapper.JsonNodeMappingSupport.asRequiredDouble; + + +/** + * A mapper from Fitbit Resource API body/log/weight responses to {@link BodyWeight} objects + * + * @author Chris Schaefbauer + */ +public class FitbitBodyWeightDataPointMapper extends FitbitDataPointMapper { + + /** + * Maps a JSON response node from the Fitbit API into a {@link BodyWeight} measure + * + * @param node a JSON node for an individual object in the "weight" array retrieved from the body/log/weight Fitbit + * API call + * @return a {@link DataPoint} object containing a {@link BodyWeight} measure with the appropriate values from the + * JSON node parameter, wrapped as an {@link Optional} + */ + @Override + protected Optional> asDataPoint(JsonNode node) { + + MassUnitValue bodyWeight = new MassUnitValue(MassUnit.KILOGRAM, asRequiredDouble(node, "weight")); + BodyWeight.Builder builder = new BodyWeight.Builder(bodyWeight); + + Optional dateTime = combineDateTimeAndTimezone(node); + + if (dateTime.isPresent()) { + builder.setEffectiveTimeFrame(dateTime.get()); + } + + Optional externalId = asOptionalLong(node, "logId"); + BodyWeight measure = builder.build(); + + return Optional.of(newDataPoint(measure, externalId.orElse(null))); + + } + + /** + * @return the name of the list node returned from Fitbit Resource API body/log/weight response + */ + @Override + protected String getListNodeName() { + return "weight"; + } +} diff --git a/shim-server/src/main/java/org/openmhealth/shim/fitbit/mapper/FitbitDataPointMapper.java b/shim-server/src/main/java/org/openmhealth/shim/fitbit/mapper/FitbitDataPointMapper.java new file mode 100644 index 00000000..0d88a943 --- /dev/null +++ b/shim-server/src/main/java/org/openmhealth/shim/fitbit/mapper/FitbitDataPointMapper.java @@ -0,0 +1,153 @@ +/* + * 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.fitbit.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.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.LocalDateTime; +import java.time.OffsetDateTime; +import java.time.ZoneOffset; +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 org.openmhealth.schema.domain.omh.DataPointHeader.Builder; +import static org.openmhealth.shim.common.mapper.JsonNodeMappingSupport.asOptionalLocalDateTime; +import static org.openmhealth.shim.common.mapper.JsonNodeMappingSupport.asRequiredNode; + + +/** + * The base class for mappers that translate Fitbit API responses to {@link Measure} objects. + * + * @author Chris Schaefbauer + * @author Emerson Farrugia + */ +public abstract class FitbitDataPointMapper implements JsonNodeDataPointMapper { + + public static final String RESOURCE_API_SOURCE_NAME = "Fitbit Resource API"; + + /** + * Maps JSON response nodes from the Fitbit API into a list of {@link DataPoint} objects with the appropriate type. + *

+ *

Data points from the Fitbit API do not have any time zone information, so these mappers use UTC as the + * timezone. There is currently no way to determine the correct time zone for a datapoint given the Fitbit API.

+ * + * @param responseNodes the list of two json nodes - the first being the get-user-info response (from + * user//profile) and the second being the specific data point of interest for the mapper + * @return a list of DataPoint objects of type T with the appropriate values mapped from the input JSON; if JSON + * objects are contained within an array in the input response, each item in that array will map into an item in + * the list + */ + @Override + public List> asDataPoints(List responseNodes) { + + checkNotNull(responseNodes); + checkArgument(responseNodes.size() == 1, "FitbitDataPointMapper requires one response node."); + + JsonNode targetTypeNodeList = asRequiredNode(responseNodes.get(0), getListNodeName()); + + List> dataPoints = Lists.newArrayList(); + + for (JsonNode targetTypeNode : targetTypeNodeList) { + asDataPoint(targetTypeNode).ifPresent(dataPoints::add); + } + + return dataPoints; + + + } + + /** + * Adds a {@link DataPointHeader} to a {@link Measure} object and wraps it as a {@link DataPoint} + * + * @param measure the body of the data point + * @param externalId the identifier of the measure as recorded by the data provider in the JSON data-point + * (optional) + * @param the measure type (e.g., StepCount, BodyMassIndex) + * @return a {@link DataPoint} object containing the body and header that map values from Fitbit API response nodes + * to schema objects + */ + protected DataPoint newDataPoint(T measure, Long externalId) { + + DataPointAcquisitionProvenance acquisitionProvenance = + new DataPointAcquisitionProvenance.Builder(RESOURCE_API_SOURCE_NAME).build(); + + if (externalId != null) { + acquisitionProvenance.setAdditionalProperty("external_id", externalId); + } + + DataPointHeader header = new Builder(UUID.randomUUID().toString(), measure.getSchemaId()) + .setAcquisitionProvenance(acquisitionProvenance).build(); + + return new DataPoint<>(header, measure); + } + + /** + * Takes a Fitbit response JSON node, which contains a date and time property, and then maps them into an {@link + * OffsetDateTime} object + * + * @return the date and time based on the "date" and "time" properties of the JsonNode parameter, wrapped as an + * {@link Optional} + */ + protected Optional combineDateTimeAndTimezone(JsonNode node) { + + Optional dateTime = asOptionalLocalDateTime(node, "date", "time"); + Optional offsetDateTime = null; + + if (dateTime.isPresent()) { + // FIXME fix the time zone offset to use the correct offset for the data point once it is fixed by Fitbit + offsetDateTime = Optional.ofNullable(OffsetDateTime.of(dateTime.get(), ZoneOffset.UTC)); + + } + + return offsetDateTime; + } + + /** + * Transforms a {@link LocalDateTime} object into an {@link OffsetDateTime} object with a UTC time zone + * + * @param dateTime local date and time for the Fitbit response JSON node + * @return the date and time based on the input dateTime parameter + */ + protected OffsetDateTime combineDateTimeAndTimezone(LocalDateTime dateTime) { + + // FIXME fix the time zone offset to use the appropriate offset for the data point once it is fixed by Fitbit + return OffsetDateTime.of(dateTime, ZoneOffset.UTC); + } + + /** + * Implemented by subclasses to map a JSON response node from the Fitbit API into a {@link Measure} object of the + * appropriate type + * + * @return a {@link DataPoint} object containing the target measure with the appropriate values from the JSON node + * parameter, wrapped as an {@link Optional} + */ + protected abstract Optional> asDataPoint(JsonNode node); + + /** + * @return the name of the list node used by this mapper + */ + protected abstract String getListNodeName(); +} diff --git a/shim-server/src/main/java/org/openmhealth/shim/fitbit/mapper/FitbitPhysicalActivityDataPointMapper.java b/shim-server/src/main/java/org/openmhealth/shim/fitbit/mapper/FitbitPhysicalActivityDataPointMapper.java new file mode 100644 index 00000000..1af35562 --- /dev/null +++ b/shim-server/src/main/java/org/openmhealth/shim/fitbit/mapper/FitbitPhysicalActivityDataPointMapper.java @@ -0,0 +1,109 @@ +/* + * 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.fitbit.mapper; + +import com.fasterxml.jackson.databind.JsonNode; +import org.openmhealth.schema.domain.omh.*; + +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.OffsetDateTime; +import java.util.Optional; + +import static org.openmhealth.shim.common.mapper.JsonNodeMappingSupport.*; + + +/** + * A mapper from Fitbit Resource API activities/date responses to {@link PhysicalActivity} objects. + * + * @author Chris Schaefbauer + */ +public class FitbitPhysicalActivityDataPointMapper extends FitbitDataPointMapper { + + /** + * Maps a JSON response node from the Fitbit API into a {@link PhysicalActivity} measure. + * + * @param node a JSON node for an individual object in the "activities" array retrieved from the activities/date/ + * Fitbit API endpoint + * @return a {@link DataPoint} object containing a {@link PhysicalActivity} measure with the appropriate values from + * the node parameter, wrapped as an {@link Optional} + */ + @Override + protected Optional> asDataPoint(JsonNode node) { + + String activityName = asRequiredString(node, "name"); + PhysicalActivity.Builder activityBuilder = new PhysicalActivity.Builder(activityName); + + Boolean hasStartTime = asRequiredBoolean(node, "hasStartTime"); + + //hasStartTime is true if the startTime value has been set, which is required of entries through the user GUI + // and from sensed data, + // however some of their data import workflows may set dummy values for these (00:00:00), in which case + // hasStartTime is false and the time shouldn't be used + if (hasStartTime) { + + Optional localStartDateTime = asOptionalLocalDateTime(node, "startDate", "startTime"); + Optional duration = asOptionalLong(node, "duration"); + + if (localStartDateTime.isPresent()) { + + OffsetDateTime offsetStartDateTime = combineDateTimeAndTimezone(localStartDateTime.get()); + if (duration.isPresent()) { + activityBuilder.setEffectiveTimeFrame(TimeInterval.ofStartDateTimeAndDuration(offsetStartDateTime, + new DurationUnitValue(DurationUnit.MILLISECOND, duration.get()))); + } + else { + activityBuilder.setEffectiveTimeFrame(offsetStartDateTime); + } + + } + } + else { + + Optional localStartDate = asOptionalLocalDate(node, "startDate"); + + if (localStartDate.isPresent()) { + //In this case we have a date, but no time, so we set the startTime to beginning of day on the + // startDate, add the offset, then set the duration as the entire day + LocalDateTime localStartDateTime = localStartDate.get().atStartOfDay(); + OffsetDateTime offsetStartDateTime = combineDateTimeAndTimezone(localStartDateTime); + activityBuilder.setEffectiveTimeFrame(TimeInterval + .ofStartDateTimeAndDuration(offsetStartDateTime, new DurationUnitValue(DurationUnit.DAY, 1))); + } + } + + Optional distance = asOptionalDouble(node, "distance"); + + if (distance.isPresent()) { + //by default fitbit returns metric unit values (https://wiki.fitbit.com/display/API/API+Unit+System), so + // this assumes that the response is using the default for distance (KM) + activityBuilder.setDistance(new LengthUnitValue(LengthUnit.KILOMETER, distance.get())); + } + + PhysicalActivity measure = activityBuilder.build(); + Optional externalId = asOptionalLong(node, "logId"); + return Optional.of(newDataPoint(measure, externalId.orElse(null))); + } + + /** + * @return the name of the list node returned from the activities/date Fitbit endpoint + */ + @Override + protected String getListNodeName() { + return "activities"; + } +} diff --git a/shim-server/src/main/java/org/openmhealth/shim/fitbit/mapper/FitbitSleepDurationDataPointMapper.java b/shim-server/src/main/java/org/openmhealth/shim/fitbit/mapper/FitbitSleepDurationDataPointMapper.java new file mode 100644 index 00000000..f5981a93 --- /dev/null +++ b/shim-server/src/main/java/org/openmhealth/shim/fitbit/mapper/FitbitSleepDurationDataPointMapper.java @@ -0,0 +1,83 @@ +/* + * 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.fitbit.mapper; + +import com.fasterxml.jackson.databind.JsonNode; +import org.openmhealth.schema.domain.omh.*; + +import java.time.LocalDateTime; +import java.time.OffsetDateTime; +import java.util.Optional; + +import static org.openmhealth.shim.common.mapper.JsonNodeMappingSupport.*; + + +/** + * A mapper from Fitbit Resource API sleep/date responses to {@link SleepDuration} objects + * + * @author Chris Schaefbauer + */ +public class FitbitSleepDurationDataPointMapper extends FitbitDataPointMapper { + + /** + * Maps a JSON response node from the Fitbit API into a {@link SleepDuration} measure + * + * @param node a JSON node for an individual object in the "sleep" array retrieved from the sleep/date/ + * Fitbit API endpoint + * @return a {@link DataPoint} object containing a {@link SleepDuration} measure with the appropriate values from + * the node parameter, wrapped as an {@link Optional} + */ + @Override + protected Optional> asDataPoint(JsonNode node) { + + DurationUnitValue unitValue = + new DurationUnitValue(DurationUnit.MINUTE, asRequiredDouble(node, "minutesAsleep")); + SleepDuration.Builder sleepDurationBuilder = new SleepDuration.Builder(unitValue); + + + Optional localStartTime = asOptionalLocalDateTime(node, "startTime"); + + if (localStartTime.isPresent()) { + + OffsetDateTime offsetStartDateTime = combineDateTimeAndTimezone(localStartTime.get()); + Optional timeInBed = asOptionalDouble(node, "timeInBed"); + + if (timeInBed.isPresent()) { + sleepDurationBuilder.setEffectiveTimeFrame(TimeInterval.ofStartDateTimeAndDuration(offsetStartDateTime, + new DurationUnitValue(DurationUnit.MINUTE, timeInBed.get()))); + } + else { + //in this case, there is no "time in bed" value, however we still have a start time, so we can set + // the datapoint to a single datetime point + sleepDurationBuilder.setEffectiveTimeFrame(offsetStartDateTime); + } + } + + SleepDuration measure = sleepDurationBuilder.build(); + + Optional externalId = asOptionalLong(node, "logId"); + return Optional.of(newDataPoint(measure, externalId.orElse(null))); + } + + /** + * @return the name of the list node returned from the sleep/date Fitbit endpoint + */ + @Override + protected String getListNodeName() { + return "sleep"; + } +} diff --git a/shim-server/src/main/java/org/openmhealth/shim/fitbit/mapper/FitbitStepCountDataPointMapper.java b/shim-server/src/main/java/org/openmhealth/shim/fitbit/mapper/FitbitStepCountDataPointMapper.java new file mode 100644 index 00000000..397ac508 --- /dev/null +++ b/shim-server/src/main/java/org/openmhealth/shim/fitbit/mapper/FitbitStepCountDataPointMapper.java @@ -0,0 +1,79 @@ +/* + * Copyright 2015 Open mHealth + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.openmhealth.shim.fitbit.mapper; + +import com.fasterxml.jackson.databind.JsonNode; +import org.openmhealth.schema.domain.omh.*; + +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.util.Optional; + +import static org.openmhealth.shim.common.mapper.JsonNodeMappingSupport.*; + + +/** + * A mapper from Fitbit Resource API activities/date responses to {@link StepCount} objects + * + * @author Chris Schaefbauer + */ +public class FitbitStepCountDataPointMapper extends FitbitDataPointMapper { + + /** + * Maps a JSON response node from the Fitbit API into a {@link StepCount} measure + * + * @param node a JSON node for an individual object in the "activities-steps" array retrieved from the + * activities/steps + * Fitbit API endpoint + * @return a {@link DataPoint} object containing a {@link StepCount} measure with the appropriate values from + * the node parameter, wrapped as an {@link Optional} + */ + @Override + protected Optional> asDataPoint(JsonNode node) { + + int stepCountValue = Integer.parseInt(asRequiredString(node, "value")); + + if (stepCountValue == 0) { + return Optional.empty(); + } + + StepCount.Builder builder = new StepCount.Builder(stepCountValue); + + Optional stepDate = asOptionalLocalDate(node, "dateTime"); + + if (stepDate.isPresent()) { + LocalDateTime startDateTime = stepDate.get().atTime(0, 0, 0, 0); + + builder.setEffectiveTimeFrame(TimeInterval.ofStartDateTimeAndDuration( + combineDateTimeAndTimezone(startDateTime), + new DurationUnitValue(DurationUnit.DAY, 1))); + + } + + StepCount measure = builder.build(); + Optional externalId = asOptionalLong(node, "logId"); + return Optional.of(newDataPoint(measure, externalId.orElse(null))); + } + + /** + * @return the name of the list node returned from the activities/steps Fitbit endpoint + */ + @Override + protected String getListNodeName() { + return "activities-steps"; + } +} diff --git a/shim-server/src/main/java/org/openmhealth/shim/googlefit/Google-Fit.md b/shim-server/src/main/java/org/openmhealth/shim/googlefit/Google-Fit.md new file mode 100644 index 00000000..9171b139 --- /dev/null +++ b/shim-server/src/main/java/org/openmhealth/shim/googlefit/Google-Fit.md @@ -0,0 +1,134 @@ +This document is a work in progress + +# api +api url: https://www.googleapis.com/fitness/v1/ + +api reference: https://developers.google.com/fit/rest/v1/reference/ + +## authentication + +### OAuth 2.0 +- protocol: OAuth 2.0 for production environments +- reference: https://developers.google.com/fit/rest/v1/authorization +- authorization URL: https://accounts.google.com/o/oauth2/auth +- access token: https://www.googleapis.com/oauth2/v3/token +- refresh token: https://www.googleapis.com/oauth2/v3/token +- scope: Scope is read and write across the different google fit domains depending on requested scope, you can request multiple scopes at once + - https://www.googleapis.com/auth/fitness.activity.read + - https://www.googleapis.com/auth/fitness.activity.write + - https://www.googleapis.com/auth/fitness.body.read + - https://www.googleapis.com/auth/fitness.body.write + - https://www.googleapis.com/auth/fitness.location.write + - https://www.googleapis.com/auth/fitness.location.read +- supports refresh tokens: yes, user access tokens expire, has a field “expires_in” in the response to the access token request +- signature placement: header signing +- access token: user access token + +# pagination +- supported: yes, responses will have a “nextPageToken” property if there is more data to be retrieved, the value of this property can be used in the header of the next response as the pageToken parameter. Requests can also have an optional “limit” parameter, however this is currently broken and will not set the nextPageToken property in the response. The whole pagination process may be currently broken. + +# rate limit +- Free quota: + - 86,400 total requests/day + - Per-user limit: 5 requests/second/user +- limit header: information not in header +- remaining header: information not in header +- next reset time header: information not in header + + +# data request limit +- unspecified + +# incremental data +datapoints have a “modifiedTimeMillis” property that lists the time they were last altered, so that datapoints can be updated if they change + +# time zone and time representation +- Most data points return datetime as unix epoch nanoseconds that are aligned to UTC + +# endpoints +In addition to the parameters listed within each specific endpoint, all endpoints accept, and in some cases require, the following request parameters: https://developers.google.com/fit/rest/v1/reference/parameters. + +## endpoint responses +The response from every endpoint is very similar and aligns with the Google RESTful API. + +Each response contains a list of data points contained in the array property named “point.” Each item in the list is a datapoint representing a measurement for the requested datatype that was reported by a device, application, or recorded directly by the user. Each data point contains the following properties: +- startTimeNanos: the start date/time timestamp in unix epoch nanos +- endTimeNanos: the end date/time timestamp in unix epoch nanos +- dataTypeName: indicates the type of data that is contained within the data point, it can be something like “com.google.height,” indicating it is a height value stored in the google fit platform or "com.google.step_count.delta” indicating it is a step count value stored in the google fit platform. The delta refers to the fact that it is the number of step counts during the specified period, since the last data point was created. +- originDataSourceId: the data source for the datapoint, representing as a namespace for the application along with some additional information. It could be a user input value, represented by the string “raw:com.google.[data-type]:com.google.android.apps.fitness:user_input” or from a third party device or third party application, such as “derived:com.google.step_count.cumulative:com.google.android.gms:samsung:Galaxy Nexus:32b1bd9e:soft_step_counter” +- value: an array that contains a single data point representing the value, can either by an intVal or fpVal property. The units for the value property are defined based on the data type in the request and documented on the Fit API website + - intVal: the value, represented as an integer point value without any decimal, for the specified data type; this property is contained within the object in the “value” array. + - fpVal: the value, represented as a floating point value with a decimal, for the specified data type; this property is contained within the object in the “value” array. +- modifiedTimeMillis: Indicates the last time this data point was modified. + +In addition to the datapoints array, there are three other properties in the response: +- minStartTimeNs: the start datetime of the request that generated the response +- maxEndTimeNs: the end datetime of the request that generated the response +- dataSourceId: the id of the data source that responded to the request + +### request parameters +The request parameters that are required and optional are the same across all data points and are: + +- Required parameters: time-frame-start-in-nanos, time-frame-end-in-nanos are required to specify the range of dates/times for which the data should be retrieved. Userid is required, but is set to “me” in all cases to refer to the user whose authentication token is being used. + +- Optional parameters: “limit”, which is the maximum number of datapoints to be returned in the response and if the there are more data points in the dataset, nextPageToken will be set in the dataset response for pagination. + +“pageToken” which is the continuation token, which is used to page through large datasets. To get the next page of a dataset, set this parameter to the value of nextPageToken from the previous response. + + +## get body weight +- Endpoint: /users/me/dataSources/derived:com.google.weight:com.google.android.gms:merge_weight/datasets/- this endpoint in theory provides all of the datapoints that have been measured by a device related to body weight. Google is not clear and does not provide documentation on their merge process. + + - Alternative: /users/me/dataSources/raw:com.google.weight:com.google.android.apps.fitness:user_input/datasets/- this endpoint provides only body weight values that were input by the user + +- Reference: https://developers.google.com/fit/rest/v1/data-types + +### description +This description relates to the primary endpoint above, for merged data points. This endpoint returns all of the body weight data points that were synced to the Google Fit platform from devices that are connected to Google Fit. The Body weight values are returned as floats in kilogram units. Each datapoint has a start datetime (startTimeNanos) and end datetime (endTimeNanos) and although they are likely the same, we will need to check that before creating the data point. The nanos values are unix epoch nanoseconds that are aligned to UTC. + +## get body height +- Endpoint: https://www.googleapis.com/fitness/v1/users/me/dataSources/derived:com.google.height:com.google.android.gms:merge_height/datasets/- + + - Alternative: /users/me/dataSources/raw:com.google.height:com.google.android.apps.fitness:user_input/datasets/- + +- Reference: https://developers.google.com/fit/rest/v1/data-types + +### description +This description relates to the primary endpoint above, for merged data points. This endpoint returns all of the body height data points that were synced to the Google Fit platform from devices connected to Google Fit. The body height values are returned as floating point numbers with a unit of meters. Each datapoint has a start datetime (startTimeNanos) and end datetime (endTimeNanos) and although they are likely the same, we will need to check that before creating the data point. The nanos values are unix epoch nanoseconds that are aligned to UTC. + +## get step count +- Endpoint: derived:com.google.step_count.delta:com.google.android.gms:merge_step_deltas + +Merge appears to combine some of the step count periods into single datapoints and then cut off one or two steps. Actually, the estimated steps breaks out some of the merged segments and adds activity estimates and does other estimating to account for anomalous steps and biking/driving steps. We use the merge_step_deltas as the merged value from across all step counter sources which will be the raw data reported which is more accurate to what the devices report which will be less confusing. + +### description +Retrieves all of the step count data points that have been recorded to the Google Fit platform, across all devices and applications that are connected to Google Play. Step counts are merged using machine learning and other approaches so that there are not duplicate step counts during the same time period. + +## get calories +- Endpoint: derived:com.google.calories.expended:com.google.android.gms:merge_calories_expended + +It appears that between these two, there is some merging of calorie counts during overlapping time periods. For example, from_activities just generates calorie estimates from actual activity entries, but ignores the calorie values that the user enters. If the user enters calories as part of entering that activity, than the user entered value overwrites the estimate from activity. + +Seems that the best approach would be to use merge_calories_expended and filter out the metabolic rate (BMR) calories, which are datapoints with a originDataSourceId of “derived:com.google.calories.expended:com.google.android.gms:from_bmr”. + +### description +Retrieves a set of ‘calories expended’ data points that is merged from all sources accessible on the Google Fit platform. The merging process is based on a set of heuristics, machine learning, and other algorithms and does not contain duplicate calories expended datapoints during any time period. The response also contains datapoints that contain ‘calories expended’ data based on basal metabolic resting rate in addition to user input data, inferred calories expended from activities, and sensed or measured data from devices. + +## get heart rate +- Endpoint: derived:com.google.heart_rate.bpm:com.google.android.gms:merge_heart_rate_bpm + +- reference: https://developers.google.com/fit/rest/v1/data-types + +### description +Retrieves heart rate measurements that have been stored on the Google Fit platform through a third-party device or application. Currently there is no way to add heart rate measurements as a user. + +## get activities +- Endpoint: derived:com.google.activity.segment:com.google.android.gms:merge_activity_segments + +- Reference: https://developers.google.com/fit/rest/v1/reference/activity-types + +### description +Retrieves information about continuous activities that were performed by the user during different time period, but not information about the outcomes of those sessions (calories burned, distance traveled, etc). These activity segments can be created through user input, application data, or inferred through sensors and other data. + +# issues +Currently the limit parameter is not working, for at least some merged datasources, and pagination does not work because the 'nextPageToken' property is not returned in responses \ No newline at end of file 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 new file mode 100644 index 00000000..71b5787d --- /dev/null +++ b/shim-server/src/main/java/org/openmhealth/shim/googlefit/GoogleFitShim.java @@ -0,0 +1,295 @@ +/* + * Copyright 2015 Open mHealth + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.openmhealth.shim.googlefit; + +import com.fasterxml.jackson.databind.JsonNode; +import org.openmhealth.shim.*; +import org.openmhealth.shim.googlefit.mapper.*; +import org.slf4j.Logger; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpMethod; +import org.springframework.http.ResponseEntity; +import org.springframework.security.oauth2.client.OAuth2RestOperations; +import org.springframework.security.oauth2.client.resource.OAuth2AccessDeniedException; +import org.springframework.security.oauth2.client.resource.OAuth2ProtectedResourceDetails; +import org.springframework.security.oauth2.client.resource.UserRedirectRequiredException; +import org.springframework.security.oauth2.client.token.AccessTokenRequest; +import org.springframework.security.oauth2.client.token.RequestEnhancer; +import org.springframework.security.oauth2.client.token.grant.code.AuthorizationCodeAccessTokenProvider; +import org.springframework.security.oauth2.common.DefaultOAuth2AccessToken; +import org.springframework.security.oauth2.common.OAuth2AccessToken; +import org.springframework.security.oauth2.common.OAuth2RefreshToken; +import org.springframework.stereotype.Component; +import org.springframework.util.MultiValueMap; +import org.springframework.util.StringUtils; +import org.springframework.web.client.HttpClientErrorException; +import org.springframework.web.client.HttpServerErrorException; +import org.springframework.web.util.UriComponentsBuilder; + +import java.net.URI; +import java.time.LocalDate; +import java.time.OffsetDateTime; +import java.time.ZoneOffset; +import java.util.Arrays; +import java.util.List; + +import static java.util.Collections.singletonList; +import static org.slf4j.LoggerFactory.getLogger; +import static org.springframework.http.ResponseEntity.ok; + + +/** + * Encapsulates parameters specific to the Google Fit REST API and processes requests for Google Fit data from shimmer. + * + * @author Eric Jain + * @author Chris Schaefbauer + */ +@Component +@ConfigurationProperties(prefix = "openmhealth.shim.googlefit") +public class GoogleFitShim extends OAuth2ShimBase { + + private static final Logger logger = getLogger(GoogleFitShim.class); + + public static final String SHIM_KEY = "googlefit"; + + private static final String DATA_URL = "https://www.googleapis.com/fitness/v1/users/me/dataSources"; + + private static final String AUTHORIZE_URL = "https://accounts.google.com/o/oauth2/auth"; + + private static final String TOKEN_URL = "https://accounts.google.com/o/oauth2/token"; + + public static final List GOOGLE_FIT_SCOPES = Arrays.asList( + "https://www.googleapis.com/auth/userinfo.email", + "https://www.googleapis.com/auth/fitness.activity.read", + "https://www.googleapis.com/auth/fitness.body.read" + ); + + @Autowired + public GoogleFitShim(ApplicationAccessParametersRepo applicationParametersRepo, + AuthorizationRequestParametersRepo authorizationRequestParametersRepo, + AccessParametersRepo accessParametersRepo, + ShimServerConfig shimServerConfig) { + super(applicationParametersRepo, authorizationRequestParametersRepo, accessParametersRepo, shimServerConfig); + } + + @Override + public String getLabel() { + return "Google Fit"; + } + + @Override + public String getShimKey() { + return SHIM_KEY; + } + + @Override + public String getBaseAuthorizeUrl() { + return AUTHORIZE_URL; + } + + @Override + public String getBaseTokenUrl() { + return TOKEN_URL; + } + + @Override + public List getScopes() { + return GOOGLE_FIT_SCOPES; + } + + public AuthorizationCodeAccessTokenProvider getAuthorizationCodeAccessTokenProvider() { + return new GoogleAuthorizationCodeAccessTokenProvider(); + } + + @Override + public ShimDataType[] getShimDataTypes() { + return new GoogleFitDataTypes[] { + GoogleFitDataTypes.ACTIVITY, + GoogleFitDataTypes.BODY_HEIGHT, + GoogleFitDataTypes.BODY_WEIGHT, + GoogleFitDataTypes.HEART_RATE, + GoogleFitDataTypes.STEP_COUNT, + GoogleFitDataTypes.CALORIES_BURNED}; + } + + public enum GoogleFitDataTypes implements ShimDataType { + + ACTIVITY("derived:com.google.activity.segment:com.google.android.gms:merge_activity_segments"), + BODY_HEIGHT("derived:com.google.height:com.google.android.gms:merge_height"), + BODY_WEIGHT("derived:com.google.weight:com.google.android.gms:merge_weight"), + HEART_RATE("derived:com.google.heart_rate.bpm:com.google.android.gms:merge_heart_rate_bpm"), + STEP_COUNT("derived:com.google.step_count.delta:com.google.android.gms:merge_step_deltas"), + CALORIES_BURNED("derived:com.google.calories.expended:com.google.android.gms:merge_calories_expended"); + + private final String streamId; + + GoogleFitDataTypes(String streamId) { + this.streamId = streamId; + + } + + public String getStreamId() { + return streamId; + } + + } + + protected ResponseEntity getData(OAuth2RestOperations restTemplate, + ShimDataRequest shimDataRequest) throws ShimException { + final GoogleFitDataTypes googleFitDataType; + try { + googleFitDataType = GoogleFitDataTypes.valueOf( + shimDataRequest.getDataTypeKey().trim().toUpperCase()); + } + catch (NullPointerException | IllegalArgumentException e) { + throw new ShimException("Null or Invalid data type parameter: " + + shimDataRequest.getDataTypeKey() + + " in shimDataRequest, cannot retrieve data."); + } + + + OffsetDateTime todayInUTC = + LocalDate.now().atStartOfDay().atOffset(ZoneOffset.UTC); + + OffsetDateTime startDateInUTC = shimDataRequest.getStartDateTime() == null ? + todayInUTC.minusDays(1) : shimDataRequest.getStartDateTime(); + long startTimeNanos = (startDateInUTC.toEpochSecond() * 1000000000) + startDateInUTC.toInstant().getNano(); + + OffsetDateTime endDateInUTC = shimDataRequest.getEndDateTime() == null ? + todayInUTC.plusDays(1) : + shimDataRequest.getEndDateTime().plusDays(1); // We are inclusive of the last day, so add 1 day to get + // the end of day on the last day, which captures the + // entire last day + long endTimeNanos = (endDateInUTC.toEpochSecond() * 1000000000) + endDateInUTC.toInstant().getNano(); + + + UriComponentsBuilder uriBuilder = UriComponentsBuilder.fromUriString(DATA_URL) + .pathSegment(googleFitDataType.getStreamId(), "datasets", "{startDate}-{endDate}"); + // TODO: Add limits back into the request once Google has fixed the 'limit' query parameter and paging + + URI uriRequest = uriBuilder.buildAndExpand(startTimeNanos, endTimeNanos).encode().toUri(); + + ResponseEntity responseEntity; + try { + responseEntity = restTemplate.getForEntity(uriRequest, JsonNode.class); + } + catch (HttpClientErrorException | HttpServerErrorException e) { + // TODO figure out how to handle this + logger.error("A request for Google Fit data failed.", e); + throw e; + } + + if (shimDataRequest.getNormalize()) { + GoogleFitDataPointMapper dataPointMapper; + switch ( googleFitDataType ) { + case BODY_WEIGHT: + dataPointMapper = new GoogleFitBodyWeightDataPointMapper(); + break; + case BODY_HEIGHT: + dataPointMapper = new GoogleFitBodyHeightDataPointMapper(); + break; + case ACTIVITY: + dataPointMapper = new GoogleFitPhysicalActivityDataPointMapper(); + break; + case STEP_COUNT: + dataPointMapper = new GoogleFitStepCountDataPointMapper(); + break; + case HEART_RATE: + dataPointMapper = new GoogleFitHeartRateDataPointMapper(); + break; + case CALORIES_BURNED: + dataPointMapper = new GoogleFitCaloriesBurnedDataPointMapper(); + break; + default: + throw new UnsupportedOperationException(); + } + + return ok().body(ShimDataResponse.result(GoogleFitShim.SHIM_KEY, dataPointMapper.asDataPoints( + singletonList(responseEntity.getBody())))); + } + else { + + return ok().body(ShimDataResponse.result(GoogleFitShim.SHIM_KEY, responseEntity.getBody())); + } + } + + @Override + protected String getAuthorizationUrl(UserRedirectRequiredException exception) { + final OAuth2ProtectedResourceDetails resource = getResource(); + + UriComponentsBuilder uriBuilder = UriComponentsBuilder + .fromUriString(exception.getRedirectUri()) + .queryParam("state", exception.getStateKey()) + .queryParam("client_id", resource.getClientId()) + .queryParam("response_type", "code") + .queryParam("access_type", "offline") + .queryParam("approval_prompt", "force") + .queryParam("scope", StringUtils.collectionToDelimitedString(resource.getScope(), " ")) + .queryParam("redirect_uri", getCallbackUrl()); + + return uriBuilder.build().encode().toUriString(); + } + + /** + * Simple overrides to base spring class from oauth. + */ + public class GoogleAuthorizationCodeAccessTokenProvider extends AuthorizationCodeAccessTokenProvider { + + public GoogleAuthorizationCodeAccessTokenProvider() { + this.setTokenRequestEnhancer(new GoogleTokenRequestEnhancer()); + } + + @Override + protected HttpMethod getHttpMethod() { + return HttpMethod.POST; + } + + @Override + public OAuth2AccessToken refreshAccessToken( + OAuth2ProtectedResourceDetails resource, + OAuth2RefreshToken refreshToken, AccessTokenRequest request) + throws UserRedirectRequiredException, + OAuth2AccessDeniedException { + OAuth2AccessToken accessToken = super.refreshAccessToken(resource, refreshToken, request); + // Google does not replace refresh tokens, so we need to hold on to the existing refresh token... + if (accessToken.getRefreshToken() == null) { + ((DefaultOAuth2AccessToken) accessToken).setRefreshToken(refreshToken); + } + return accessToken; + } + } + + + /** + * Adds parameters required by Google to authorization token requests. + */ + private class GoogleTokenRequestEnhancer implements RequestEnhancer { + + @Override + public void enhance(AccessTokenRequest request, + OAuth2ProtectedResourceDetails resource, + MultiValueMap form, HttpHeaders headers) { + form.set("client_id", resource.getClientId()); + form.set("client_secret", resource.getClientSecret()); + if (request.getStateKey() != null) { + form.set("redirect_uri", getCallbackUrl()); + } + } + } +} 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 new file mode 100644 index 00000000..990e1a31 --- /dev/null +++ b/shim-server/src/main/java/org/openmhealth/shim/googlefit/mapper/GoogleFitBodyHeightDataPointMapper.java @@ -0,0 +1,58 @@ +/* + * Copyright 2015 Open mHealth + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.openmhealth.shim.googlefit.mapper; + +import com.fasterxml.jackson.databind.JsonNode; +import org.openmhealth.schema.domain.omh.BodyHeight; +import org.openmhealth.schema.domain.omh.DataPoint; +import org.openmhealth.schema.domain.omh.LengthUnitValue; + +import java.util.Optional; + +import static org.openmhealth.schema.domain.omh.LengthUnit.METER; +import static org.openmhealth.shim.common.mapper.JsonNodeMappingSupport.*; + + +/** + * A mapper from Google Fit "merged height" (derived:com.google.weight:com.google.android.gms:merge_height) endpoint + * responses to {@link BodyHeight} objects. + * + * @author Chris Schaefbauer + * @see Google Fit Data Type Documentation + */ +public class GoogleFitBodyHeightDataPointMapper extends GoogleFitDataPointMapper { + + @Override + public Optional> asDataPoint(JsonNode listNode) { + + JsonNode valueListNode = asRequiredNode(listNode, getValueListNodeName()); + double bodyHeightValue = asRequiredDouble(valueListNode.get(0), "fpVal"); + + if (bodyHeightValue == 0) { + return Optional.empty(); + } + + BodyHeight.Builder bodyHeightBuilder = new BodyHeight.Builder(new LengthUnitValue(METER, bodyHeightValue)); + + setEffectiveTimeFrameIfPresent(bodyHeightBuilder, listNode); + + BodyHeight bodyHeight = bodyHeightBuilder.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 new file mode 100644 index 00000000..d81465de --- /dev/null +++ b/shim-server/src/main/java/org/openmhealth/shim/googlefit/mapper/GoogleFitBodyWeightDataPointMapper.java @@ -0,0 +1,58 @@ +/* + * Copyright 2015 Open mHealth + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.openmhealth.shim.googlefit.mapper; + +import com.fasterxml.jackson.databind.JsonNode; +import org.openmhealth.schema.domain.omh.BodyWeight; +import org.openmhealth.schema.domain.omh.DataPoint; +import org.openmhealth.schema.domain.omh.MassUnitValue; + +import java.util.Optional; + +import static org.openmhealth.schema.domain.omh.MassUnit.KILOGRAM; +import static org.openmhealth.shim.common.mapper.JsonNodeMappingSupport.*; + + +/** + * A mapper from Google Fit "merged weight" endpoint responses + * (derived:com.google.weight:com.google.android.gms:merge_weight) to {@link BodyWeight} + * objects. + * + * @author Chris Schaefbauer + * @see Google Fit Data Type Documentation + */ +public class GoogleFitBodyWeightDataPointMapper extends GoogleFitDataPointMapper { + + @Override + public Optional> asDataPoint(JsonNode listNode) { + + JsonNode valueList = asRequiredNode(listNode, getValueListNodeName()); + + Double bodyWeightValue = asRequiredDouble(valueList.get(0), "fpVal"); + if (bodyWeightValue == 0) { + return Optional.empty(); + } + + BodyWeight.Builder bodyWeightBuilder = new BodyWeight.Builder(new MassUnitValue(KILOGRAM, bodyWeightValue)); + setEffectiveTimeFrameIfPresent(bodyWeightBuilder, listNode); + + Optional originDataSourceId = asOptionalString(listNode, "originDataSourceId"); + + BodyWeight bodyWeight = bodyWeightBuilder.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 new file mode 100644 index 00000000..d0cc1b54 --- /dev/null +++ b/shim-server/src/main/java/org/openmhealth/shim/googlefit/mapper/GoogleFitCaloriesBurnedDataPointMapper.java @@ -0,0 +1,64 @@ +/* + * Copyright 2015 Open mHealth + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.openmhealth.shim.googlefit.mapper; + +import com.fasterxml.jackson.databind.JsonNode; +import org.openmhealth.schema.domain.omh.CaloriesBurned; +import org.openmhealth.schema.domain.omh.DataPoint; +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.*; + + +/** + * 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. + * + * @author Chris Schaefbauer + * @see Google Fit Data Type Documentation + */ +public class GoogleFitCaloriesBurnedDataPointMapper extends GoogleFitDataPointMapper { + + @Override + 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)); + + setEffectiveTimeFrameIfPresent(caloriesBurnedBuilder, listNode); + + 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 + if (originDataSourceId.isPresent()) { + if (originDataSourceId.get().contains("bmr")) { + return Optional.empty(); + } + } + return Optional.of(newDataPoint(caloriesBurned, originDataSourceId.orElse(null))); + } +} 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 new file mode 100644 index 00000000..952271f2 --- /dev/null +++ b/shim-server/src/main/java/org/openmhealth/shim/googlefit/mapper/GoogleFitDataPointMapper.java @@ -0,0 +1,167 @@ +/* + * Copyright 2015 Open mHealth + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.openmhealth.shim.googlefit.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 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 org.openmhealth.shim.common.mapper.JsonNodeMappingSupport.asOptionalNode; +import static org.openmhealth.shim.common.mapper.JsonNodeMappingSupport.asOptionalString; + + +/** + * A base class for mappers that translate Google Fit API responses into {@link Measure} objects. + * + * @author Chris Schaefbauer + */ +public abstract class GoogleFitDataPointMapper implements JsonNodeDataPointMapper { + + public static final String RESOURCE_API_SOURCE_NAME = "Google Fit API"; + + /** + * 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, + * "point," and then iteratively maps the nodes in the list. + * + * @param responseNodes the response body from a Google Fit endpoint, contained in a list of a single JSON node + * @return a list of DataPoint objects of type T with the appropriate values mapped from the input JSON; because + * these JSON objects are contained within an array in the input response, each object in that JSON array will map + * to an item in the returned list + */ + public List> asDataPoints(List responseNodes) { + + checkNotNull(responseNodes); + checkArgument(responseNodes.size() == 1, "Only one response should be input to the mapper"); + + List> dataPoints = Lists.newArrayList(); + Optional listNodes = asOptionalNode(responseNodes.get(0), getListNodeName()); + if (listNodes.isPresent()) { + for (JsonNode listNode : listNodes.get()) { + asDataPoint(listNode).ifPresent(dataPoints::add); + } + } + + return dataPoints; + + } + + /** + * Maps a JSON response node from the Google Fit API into a {@link Measure} object of the appropriate type. + * + * @param listNode an individual datapoint from the array contained in the Google Fit response + * @return a {@link DataPoint} object containing the target measure with the appropriate values from the JSON node + * parameter, wrapped as an {@link Optional} + */ + protected abstract Optional> asDataPoint(JsonNode listNode); + + + /** + * Creates a complete {@link DataPoint} object with the measure parameter and the appropriate header and header + * information. + * + * @param measure the {@link Measure} of type T to be wrapped as a {@link DataPoint} + * @param fitDataSourceId the origin data source from the Google Fit API, contained in the originDataSourceId + * property; refers to the originating source that brought the data into Google Fit + */ + public DataPoint newDataPoint(T measure, String fitDataSourceId) { + + DataPointAcquisitionProvenance.Builder acquisitionProvenanceBuilder = + new DataPointAcquisitionProvenance.Builder(RESOURCE_API_SOURCE_NAME); + + // For data from the Google Fit API that has an origin data source id ending with "user_input" we know that + // the data point is self-reported from the user through the Google Fit app or web interface + if (fitDataSourceId != null && fitDataSourceId.endsWith("user_input")) { + acquisitionProvenanceBuilder.setModality(DataPointModality.SELF_REPORTED); + } + + // Although there is limited standardization in this information, we decided to pass it through as an + // additional property to prevent information loss and in case someone were to leverage this information in + // some way + DataPointAcquisitionProvenance acquisitionProvenance = acquisitionProvenanceBuilder.build(); + if (fitDataSourceId != null) { + acquisitionProvenance.setAdditionalProperty("source_origin_id", fitDataSourceId); + } + + DataPointHeader header = new DataPointHeader.Builder(UUID.randomUUID().toString(), measure.getSchemaId()). + setAcquisitionProvenance(acquisitionProvenance).build(); + + return new DataPoint<>(header, measure); + } + + /** + * Converts a nanosecond timestamp from the Google Fit API into an offset datetime value. + * + * @param unixEpochNanosString the timestamp directly from the Google JSON document + * @return an offset datetime object representing the input timestamp + */ + public OffsetDateTime convertGoogleNanosToOffsetDateTime(String unixEpochNanosString) { + + return OffsetDateTime.ofInstant(Instant.ofEpochSecond(0, Long.parseLong(unixEpochNanosString)), ZoneId.of("Z")); + } + + /** + * @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 + */ + public void setEffectiveTimeFrameIfPresent(T.Builder builder, JsonNode listNode) { + + Optional startTimeNanosString = asOptionalString(listNode, "startTimeNanos"); + Optional endTimeNanosString = asOptionalString(listNode, "endTimeNanos"); + + // 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())); + + } + else { + builder.setEffectiveTimeFrame(TimeInterval.ofStartDateTimeAndEndDateTime( + convertGoogleNanosToOffsetDateTime(startTimeNanosString.get()), + convertGoogleNanosToOffsetDateTime(endTimeNanosString.get()))); + } + + } + } + + /** + * The name of the list that contains the datapoints associated with the request. + */ + protected String getListNodeName() { + return "point"; + } + + /** + * The name of the list node contained within each datapoint that contains the target value. + */ + protected String getValueListNodeName() { + return "value"; + } + +} 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 new file mode 100644 index 00000000..ee715492 --- /dev/null +++ b/shim-server/src/main/java/org/openmhealth/shim/googlefit/mapper/GoogleFitHeartRateDataPointMapper.java @@ -0,0 +1,51 @@ +/* + * Copyright 2015 Open mHealth + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.openmhealth.shim.googlefit.mapper; + +import com.fasterxml.jackson.databind.JsonNode; +import org.openmhealth.schema.domain.omh.DataPoint; +import org.openmhealth.schema.domain.omh.HeartRate; + +import java.util.Optional; + +import static org.openmhealth.shim.common.mapper.JsonNodeMappingSupport.*; + + +/** + * A mapper from Google Fit merge heart rate endpoint responses (derived:com.google.heart_rate.bpm:com.google.android + * .gms:merge_heart_rate_bpm) to {@link HeartRate} objects. + * + * @author Chris Schaefbauer + * @see Google Fit Data Type Documentation + */ +public class GoogleFitHeartRateDataPointMapper extends GoogleFitDataPointMapper { + + @Override + 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(); + 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 new file mode 100644 index 00000000..fe5ce23c --- /dev/null +++ b/shim-server/src/main/java/org/openmhealth/shim/googlefit/mapper/GoogleFitPhysicalActivityDataPointMapper.java @@ -0,0 +1,211 @@ +/* + * Copyright 2015 Open mHealth + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.openmhealth.shim.googlefit.mapper; + +import com.fasterxml.jackson.databind.JsonNode; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; +import org.openmhealth.schema.domain.omh.DataPoint; +import org.openmhealth.schema.domain.omh.PhysicalActivity; + +import java.util.Optional; + +import static org.openmhealth.shim.common.mapper.JsonNodeMappingSupport.*; + + +/** + * A mapper from Google Fit "merged activity segment" (derived:com.google.activity.segment:com.google.android + * .gms:merge_activity_segments) endpoint responses to {@link PhysicalActivity} objects. + * + * @author Chris Schaefbauer + * @see Google Fit Data Type Documentation + */ +public class GoogleFitPhysicalActivityDataPointMapper extends GoogleFitDataPointMapper { + + protected ImmutableMap googleFitDataTypes; + protected ImmutableList sleepActivityTypes; + protected ImmutableList stationaryActivityTypes; + + public GoogleFitPhysicalActivityDataPointMapper() { + + initializeActivityMap(); + initializeActivityTypesToSkip(); + } + + /** + * Maps a JSON response node from the Google Fit API to a {@link PhysicalActivity} measure. + * + * @param listNode an individual datapoint from the array in the Google Fit response + * @return a {@link DataPoint} object containing a {@link PhysicalActivity} measure with the appropriate values from + * the JSON node parameter, wrapped as an {@link Optional} + */ + @Override + protected Optional> asDataPoint(JsonNode listNode) { + + JsonNode listValueNode = asRequiredNode(listNode, "value"); + long activityTypeId = asRequiredLong(listValueNode.get(0), "intVal"); + + // This means that the activity was actually sleep, which should be captured using sleep duration, or + // stationary, which should not be captured as it is the absence of activity + if (sleepActivityTypes.contains((int) activityTypeId) || + stationaryActivityTypes.contains((int) activityTypeId)) { + + return Optional.empty(); + } + + String activityName = googleFitDataTypes.get((int) activityTypeId); + PhysicalActivity.Builder physicalActivityBuilder = new PhysicalActivity.Builder( + activityName); + setEffectiveTimeFrameIfPresent(physicalActivityBuilder, listNode); + PhysicalActivity physicalActivity = physicalActivityBuilder.build(); + Optional originSourceId = asOptionalString(listNode, "originDataSourceId"); + return Optional.of(newDataPoint(physicalActivity, originSourceId.orElse(null))); + + } + + /** + * Loads an immutable list with the activity type identifiers that represent different types of sleeping. + */ + private void initializeActivityTypesToSkip() { + + sleepActivityTypes = ImmutableList.of(72, 109, 110, 111, 112); + stationaryActivityTypes = ImmutableList.of(3); + } + + /** + * Map between integer values and the activity names that they represent. + * + * @see Google Fit Activity Types + */ + private void initializeActivityMap() { + + ImmutableMap.Builder activityDataTypeBuilder = ImmutableMap.builder(); + activityDataTypeBuilder.put(9, "Aerobics") + .put(10, "Badminton") + .put(11, "Baseball") + .put(12, "Basketball") + .put(13, "Biathlon") + .put(1, "Biking") + .put(14, "Handbiking") + .put(15, "Mountain biking") + .put(16, "Road biking") + .put(17, "Spinning") + .put(18, "Stationary biking") + .put(19, "Utility biking") + .put(20, "Boxing") + .put(21, "Calisthenics") + .put(22, "Circuit training") + .put(23, "Cricket") + .put(106, "Curling") + .put(24, "Dancing") + .put(102, "Diving") + .put(25, "Elliptical") + .put(103, "Ergometer") + .put(26, "Fencing") + .put(27, "Football (American)") + .put(28, "Football (Australian)") + .put(29, "Football (Soccer)") + .put(30, "Frisbee") + .put(31, "Gardening") + .put(32, "Golf") + .put(33, "Gymnastics") + .put(34, "Handball") + .put(35, "Hiking") + .put(36, "Hockey") + .put(37, "Horseback riding") + .put(38, "Housework") + .put(104, "Ice skating") + .put(0, "In vehicle") + .put(39, "Jumping rope") + .put(40, "Kayaking") + .put(41, "Kettlebell training") + .put(42, "Kickboxing") + .put(43, "Kitesurfing") + .put(44, "Martial arts") + .put(45, "Meditation") + .put(46, "Mixed martial arts") + .put(2, "On foot") + .put(47, "P90X exercises") + .put(48, "Paragliding") + .put(49, "Pilates") + .put(50, "Polo") + .put(51, "Racquetball") + .put(52, "Rock climbing") + .put(53, "Rowing") + .put(54, "Rowing machine") + .put(55, "Rugby") + .put(8, "Running") + .put(56, "Jogging") + .put(57, "Running on sand") + .put(58, "Running (treadmill)") + .put(59, "Sailing") + .put(60, "Scuba diving") + .put(61, "Skateboarding") + .put(62, "Skating") + .put(63, "Cross skating") + .put(105, "Indoor skating") + .put(64, "Inline skating (rollerblading)") + .put(65, "Skiing") + .put(66, "Back-country skiing") + .put(67, "Cross-country skiing") + .put(68, "Downhill skiing") + .put(69, "Kite skiing") + .put(70, "Roller skiing") + .put(71, "Sledding") + .put(72, "Sleeping") + .put(73, "Snowboarding") + .put(74, "Snowmobile") + .put(75, "Snowshoeing") + .put(76, "Squash") + .put(77, "Stair climbing") + .put(78, "Stair-climbing machine") + .put(79, "Stand-up paddleboarding") + .put(3, "Still (not moving)") + .put(80, "Strength training") + .put(81, "Surfing") + .put(82, "Swimming") + .put(84, "Swimming (open water)") + .put(83, "Swimming (swimming pool)") + .put(85, "Table tenis (ping pong)") + .put(86, "Team sports") + .put(87, "Tennis") + .put(5, "Tilting (sudden device gravity change)") + .put(88, "Treadmill (walking or running)") + .put(4, "Unknown (unable to detect activity)") + .put(89, "Volleyball") + .put(90, "Volleyball (beach)") + .put(91, "Volleyball (indoor)") + .put(92, "Wakeboarding") + .put(7, "Walking") + .put(93, "Walking (fitness)") + .put(94, "Nording walking") + .put(95, "Walking (treadmill)") + .put(96, "Waterpolo") + .put(97, "Weightlifting") + .put(98, "Wheelchair") + .put(99, "Windsurfing") + .put(100, "Yoga") + .put(101, "Zumba") + .put(108, "Other") + .put(109, "Light sleep") + .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/GoogleFitStepCountDataPointMapper.java b/shim-server/src/main/java/org/openmhealth/shim/googlefit/mapper/GoogleFitStepCountDataPointMapper.java new file mode 100644 index 00000000..d21e6913 --- /dev/null +++ b/shim-server/src/main/java/org/openmhealth/shim/googlefit/mapper/GoogleFitStepCountDataPointMapper.java @@ -0,0 +1,52 @@ +/* + * Copyright 2015 Open mHealth + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.openmhealth.shim.googlefit.mapper; + +import com.fasterxml.jackson.databind.JsonNode; +import org.openmhealth.schema.domain.omh.DataPoint; +import org.openmhealth.schema.domain.omh.StepCount; + +import java.util.Optional; + +import static org.openmhealth.shim.common.mapper.JsonNodeMappingSupport.*; + + +/** + * 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 StepCount} + * objects. + * + * @author Chris Schaefbauer + * @see Google Fit Data Type Documentation + */ +public class GoogleFitStepCountDataPointMapper extends GoogleFitDataPointMapper { + + @Override + protected Optional> asDataPoint(JsonNode listNode) { + + JsonNode listValueNode = asRequiredNode(listNode, "value"); + long stepCountValue = asRequiredLong(listValueNode.get(0), "intVal"); + if (stepCountValue == 0) { + return Optional.empty(); + } + StepCount.Builder stepCountBuilder = new StepCount.Builder(stepCountValue); + setEffectiveTimeFrameIfPresent(stepCountBuilder, listNode); + StepCount stepCount = stepCountBuilder.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/healthvault/HealthvaultConfig.java b/shim-server/src/main/java/org/openmhealth/shim/healthvault/HealthvaultConfig.java deleted file mode 100644 index a400cac2..00000000 --- a/shim-server/src/main/java/org/openmhealth/shim/healthvault/HealthvaultConfig.java +++ /dev/null @@ -1,52 +0,0 @@ -/* - * Copyright 2014 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.healthvault; - -import org.openmhealth.shim.ApplicationAccessParameters; -import org.openmhealth.shim.ApplicationAccessParametersRepo; -import org.openmhealth.shim.ShimConfig; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.context.properties.ConfigurationProperties; -import org.springframework.stereotype.Component; - -/** - * @author Danilo Bonilla - */ -@Component -@ConfigurationProperties(prefix = "openmhealth.shim.healthvault") -public class HealthvaultConfig implements ShimConfig { - - private String clientId; - - @Autowired - private ApplicationAccessParametersRepo applicationParametersRepo; - - public String getClientId() { - ApplicationAccessParameters parameters = - applicationParametersRepo.findByShimKey(HealthvaultShim.SHIM_KEY); - return parameters != null ? parameters.getClientId() : clientId; - } - - @Override - public String getClientSecret() { - return null; //Not required by health vault. - } - - public void setClientId(String clientId) { - this.clientId = clientId; - } -} diff --git a/shim-server/src/main/java/org/openmhealth/shim/healthvault/HealthvaultShim.java b/shim-server/src/main/java/org/openmhealth/shim/healthvault/HealthvaultShim.java.txt similarity index 95% rename from shim-server/src/main/java/org/openmhealth/shim/healthvault/HealthvaultShim.java rename to shim-server/src/main/java/org/openmhealth/shim/healthvault/HealthvaultShim.java.txt index 7fbc5da5..0a747ff5 100644 --- a/shim-server/src/main/java/org/openmhealth/shim/healthvault/HealthvaultShim.java +++ b/shim-server/src/main/java/org/openmhealth/shim/healthvault/HealthvaultShim.java.txt @@ -34,6 +34,9 @@ import org.openmhealth.schema.pojos.*; import org.openmhealth.schema.pojos.generic.MassUnitValue; import org.openmhealth.shim.*; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.stereotype.Component; import org.springframework.util.CollectionUtils; import org.w3c.dom.Document; import org.w3c.dom.Node; @@ -50,6 +53,7 @@ import javax.xml.transform.stream.StreamResult; import javax.xml.xpath.XPath; import javax.xml.xpath.XPathFactory; + import java.io.IOException; import java.io.InputStream; import java.io.StringWriter; @@ -65,7 +69,9 @@ * * @author Danilo Bonilla */ -public class HealthvaultShim implements Shim { +@Component +@ConfigurationProperties(prefix = "openmhealth.shim.healthvault") +public class HealthvaultShim extends ShimBase { public static final String SHIM_KEY = "healthvault"; @@ -86,16 +92,16 @@ public class HealthvaultShim implements Shim { private ShimServerConfig shimServerConfig; - private HealthvaultConfig config; - - public HealthvaultShim(AuthorizationRequestParametersRepo authorizationRequestParametersRepo, - ShimServerConfig shimServerConfig, - HealthvaultConfig healthvaultConfig) { + @Autowired + public HealthvaultShim(ApplicationAccessParametersRepo applicationParametersRepo, + AuthorizationRequestParametersRepo authorizationRequestParametersRepo, + ShimServerConfig shimServerConfig) { + super(applicationParametersRepo); this.authorizationRequestParametersRepo = authorizationRequestParametersRepo; this.shimServerConfig = shimServerConfig; - this.config = healthvaultConfig; - if(config != null && config.getClientId() != null){ - this.connection = ConnectionFactory.getConnection(config.getClientId()); + String apiKey = findApplicationAccessParameters().getClientId(); + if (apiKey != null){ + this.connection = ConnectionFactory.getConnection(apiKey); } } @@ -110,8 +116,9 @@ public String getShimKey() { } @Override - public String getClientId() { - return config.getClientId(); + public boolean isConfigured() { + ApplicationAccessParameters parameters = findApplicationAccessParameters(); + return parameters.getClientId() != null; } @Override @@ -546,7 +553,7 @@ public AuthorizationResponse handleAuthorizationResponse( final String recordId = getSelectedRecordId(accessToken); AccessParameters accessParameters = new AccessParameters(); - accessParameters.setClientId(getClientId()); + accessParameters.setClientId(findApplicationAccessParameters().getClientId()); accessParameters.setStateKey(stateKey); accessParameters.setUsername(authParams.getUsername()); accessParameters.setAccessToken(accessToken); @@ -559,7 +566,7 @@ public AuthorizationResponse handleAuthorizationResponse( private String getAuthorizationUrl(String redirectUrl, String actionQs) { return getBaseAuthorizeUrl() - + "/redirect.aspx?target=AUTH&targetqs=?appid=" + getClientId() + + "/redirect.aspx?target=AUTH&targetqs=?appid=" + findApplicationAccessParameters().getClientId() + "%26redirect=" + redirectUrl + "%26actionqs=" + actionQs; } @@ -653,7 +660,7 @@ public T makeRequest(AccessParameters accessInfo, accessInfo.getAdditionalParameters().get(RECORD_ID_PARAM).toString()); HVAccessor accessor = new HVAccessor(); accessor.send(request, - ConnectionFactory.getConnection(config.getClientId())); + ConnectionFactory.getConnection(findApplicationAccessParameters().getClientId())); try { InputStream istream = accessor.getResponse().getInputStream(); return marshaller.marshal(istream); @@ -666,11 +673,6 @@ public T makeRequest(AccessParameters accessInfo, } } - @Override - public String getClientSecret() { - return null; //NOOP - } - @Override public String getBaseTokenUrl() { return null; //NOOP diff --git a/shim-server/src/main/java/org/openmhealth/shim/ihealth/IHealthShim.java.txt b/shim-server/src/main/java/org/openmhealth/shim/ihealth/IHealthShim.java.txt new file mode 100644 index 00000000..1815fefe --- /dev/null +++ b/shim-server/src/main/java/org/openmhealth/shim/ihealth/IHealthShim.java.txt @@ -0,0 +1,414 @@ +package org.openmhealth.shim.ihealth; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Date; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.joda.time.DateTime; +import org.joda.time.DateTimeZone; +import org.openmhealth.schema.pojos.Activity; +import org.openmhealth.schema.pojos.BloodGlucose; +import org.openmhealth.schema.pojos.BloodPressure; +import org.openmhealth.schema.pojos.BodyWeight; +import org.openmhealth.schema.pojos.DataPoint; +import org.openmhealth.schema.pojos.HeartRate; +import org.openmhealth.schema.pojos.SleepDuration; +import org.openmhealth.schema.pojos.StepCount; +import org.openmhealth.schema.pojos.build.ActivityBuilder; +import org.openmhealth.schema.pojos.build.BloodGlucoseBuilder; +import org.openmhealth.schema.pojos.build.BloodPressureBuilder; +import org.openmhealth.schema.pojos.build.BodyWeightBuilder; +import org.openmhealth.schema.pojos.build.HeartRateBuilder; +import org.openmhealth.schema.pojos.build.SleepDurationBuilder; +import org.openmhealth.schema.pojos.build.StepCountBuilder; +import org.openmhealth.schema.pojos.generic.DurationUnitValue; +import org.openmhealth.shim.AccessParametersRepo; +import org.openmhealth.shim.ApplicationAccessParametersRepo; +import org.openmhealth.shim.AuthorizationRequestParametersRepo; +import org.openmhealth.shim.OAuth2ShimBase; +import org.openmhealth.shim.ShimDataRequest; +import org.openmhealth.shim.ShimDataResponse; +import org.openmhealth.shim.ShimDataType; +import org.openmhealth.shim.ShimException; +import org.openmhealth.shim.ShimServerConfig; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpMethod; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.http.client.ClientHttpResponse; +import org.springframework.security.oauth2.client.OAuth2RestOperations; +import org.springframework.security.oauth2.client.resource.OAuth2ProtectedResourceDetails; +import org.springframework.security.oauth2.client.resource.UserRedirectRequiredException; +import org.springframework.security.oauth2.client.token.AccessTokenRequest; +import org.springframework.security.oauth2.client.token.RequestEnhancer; +import org.springframework.security.oauth2.client.token.grant.code.AuthorizationCodeAccessTokenProvider; +import org.springframework.security.oauth2.client.token.grant.code.AuthorizationCodeResourceDetails; +import org.springframework.security.oauth2.common.AuthenticationScheme; +import org.springframework.security.oauth2.common.DefaultOAuth2AccessToken; +import org.springframework.security.oauth2.common.DefaultOAuth2RefreshToken; +import org.springframework.security.oauth2.common.OAuth2AccessToken; +import org.springframework.security.oauth2.common.util.SerializationUtils; +import org.springframework.stereotype.Component; +import org.springframework.util.MultiValueMap; +import org.springframework.web.client.ResponseExtractor; +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.databind.DeserializationContext; +import com.fasterxml.jackson.databind.JsonDeserializer; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.module.SimpleModule; +import com.google.common.base.Joiner; +import com.google.common.base.Preconditions; +import com.google.common.base.Strings; +import com.google.common.collect.ImmutableMap; + +@Component +@ConfigurationProperties(prefix = "openmhealth.shim.ihealth") +public class IHealthShim extends OAuth2ShimBase { + + public static final String SHIM_KEY = "ihealth"; + + private static final String API_URL = "https://api.ihealthlabs.com:8443/openapiv2"; + + private static final String AUTHORIZE_URL = "https://api.ihealthlabs.com:8443/OpenApiV2/OAuthv2/userauthorization/"; + + private static final String TOKEN_URL = AUTHORIZE_URL; + + public static final List IHEALTH_SCOPES = Arrays.asList("OpenApiActivity", "OpenApiBP", "OpenApiSleep", + "OpenApiWeight", "OpenApiBG", "OpenApiSpO2", "OpenApiUserInfo", "OpenApiFood", "OpenApiSport"); + + @Autowired + public IHealthShim(ApplicationAccessParametersRepo applicationParametersRepo, + AuthorizationRequestParametersRepo authorizationRequestParametersRepo, + AccessParametersRepo accessParametersRepo, + ShimServerConfig shimServerConfig) { + super(applicationParametersRepo, authorizationRequestParametersRepo, accessParametersRepo, shimServerConfig); + } + + @Override + public String getLabel() { + return "iHealth"; + } + + @Override + public String getShimKey() { + return SHIM_KEY; + } + + @Override + public String getBaseAuthorizeUrl() { + return AUTHORIZE_URL; + } + + @Override + public String getBaseTokenUrl() { + return TOKEN_URL; + } + + @Override + public List getScopes() { + return IHEALTH_SCOPES; + } + + @Override + public AuthorizationCodeAccessTokenProvider getAuthorizationCodeAccessTokenProvider() { + return new IHealthAuthorizationCodeAccessTokenProvider(); + } + + @Override + public ShimDataType[] getShimDataTypes() { + return new ShimDataType[] { + IHealthDataTypes.ACTIVITY, + IHealthDataTypes.BLOOD_GLUCOSE, + IHealthDataTypes.BLOOD_PRESSURE, + IHealthDataTypes.BODY_WEIGHT, + IHealthDataTypes.SLEEP, + IHealthDataTypes.STEP_COUNT + }; + } + + public enum IHealthDataTypes implements ShimDataType { + + ACTIVITY("sport", new JsonDeserializer() { + @Override + public ShimDataResponse deserialize(JsonParser jsonParser, DeserializationContext deserializationContext) + throws IOException { + + JsonNode responseNode = jsonParser.getCodec().readTree(jsonParser); + if (responseNode.path("SPORTDataList").size() == 0) { + return ShimDataResponse.empty(IHealthShim.SHIM_KEY); + } + List activities = new ArrayList<>(); + for (JsonNode node : responseNode.path("SPORTDataList")) { + activities.add(new ActivityBuilder() + .withStartAndEnd(dateTimeValue(node.path("SportStartTime")), dateTimeValue(node.path("SportEndTime"))) + .setActivityName(textValue(node.path("SportName"))).build()); + } + Map results = new HashMap<>(); + results.put(Activity.SCHEMA_ACTIVITY, activities); + return ShimDataResponse.result(IHealthShim.SHIM_KEY, results); + } + }), + + BLOOD_GLUCOSE("glucose", new JsonDeserializer() { + @Override + public ShimDataResponse deserialize(JsonParser jsonParser, DeserializationContext deserializationContext) + throws IOException { + + JsonNode responseNode = jsonParser.getCodec().readTree(jsonParser); + if (responseNode.path("BGDataList").size() == 0) { + return ShimDataResponse.empty(IHealthShim.SHIM_KEY); + } + List datapoints = new ArrayList<>(); + for (JsonNode node : responseNode.path("BGDataList")) { + datapoints.add(new BloodGlucoseBuilder().setTimeTaken(dateTimeValue(node.path("MDate"))) + .setMgdLValue(node.path("BG").decimalValue()) + .setNotes(textValue(node.path("Note"))).build()); + } + Map results = new HashMap<>(); + results.put(BloodGlucose.SCHEMA_BLOOD_GLUCOSE, datapoints); + return ShimDataResponse.result(IHealthShim.SHIM_KEY, results); + } + }), + + BLOOD_PRESSURE("bp", new JsonDeserializer() { + @Override + public ShimDataResponse deserialize(JsonParser jsonParser, DeserializationContext deserializationContext) + throws IOException { + + JsonNode responseNode = jsonParser.getCodec().readTree(jsonParser); + if (responseNode.path("BPDataList").size() == 0) { + return ShimDataResponse.empty(IHealthShim.SHIM_KEY); + } + List bloodPressure = new ArrayList<>(); + List heartRate = new ArrayList<>(); + for (JsonNode node : responseNode.path("BPDataList")) { + DateTime start = dateTimeValue(node.path("MDate")); + bloodPressure.add(new BloodPressureBuilder().setTimeTaken(start) + .setValues(node.path("HP").decimalValue(), node.path("LP").decimalValue()) + .setNotes(textValue(node.path("Note"))).build()); + heartRate.add(new HeartRateBuilder().withTimeTaken(start) + .withRate(node.path("HR").intValue()).build()); + } + Map results = new HashMap<>(); + results.put(BloodPressure.SCHEMA_BLOOD_PRESSURE, bloodPressure); + results.put(HeartRate.SCHEMA_HEART_RATE, heartRate); + return ShimDataResponse.result(IHealthShim.SHIM_KEY, results); + } + }), + + BODY_WEIGHT("weight", new JsonDeserializer() { + @Override + public ShimDataResponse deserialize(JsonParser jsonParser, DeserializationContext deserializationContext) + throws IOException { + + JsonNode responseNode = jsonParser.getCodec().readTree(jsonParser); + if (responseNode.path("WeightDataList").size() == 0) { + return ShimDataResponse.empty(IHealthShim.SHIM_KEY); + } + List datapoints = new ArrayList<>(); + for (JsonNode node : responseNode.path("WeightDataList")) { + datapoints.add(new BodyWeightBuilder().setTimeTaken(dateTimeValue(node.path("MDate"))) + .setWeight(node.path("WeightValue").asText(), "kg").build()); + } + Map results = new HashMap<>(); + results.put(BodyWeight.SCHEMA_BODY_WEIGHT, datapoints); + return ShimDataResponse.result(IHealthShim.SHIM_KEY, results); + } + }), + + SLEEP("sleep", new JsonDeserializer() { + @Override + public ShimDataResponse deserialize(JsonParser jsonParser, DeserializationContext deserializationContext) + throws IOException { + + JsonNode responseNode = jsonParser.getCodec().readTree(jsonParser); + if (responseNode.path("SRDataList").size() == 0) { + return ShimDataResponse.empty(IHealthShim.SHIM_KEY); + } + List sleepDurations = new ArrayList<>(); + for (JsonNode node : responseNode.path("SRDataList")) { + DateTime start = dateTimeValue(node.path("StartTime")); + DateTime end = dateTimeValue(node.path("EndTime")); + sleepDurations.add(new SleepDurationBuilder().withStartAndEnd(start, end) + .setNotes(textValue(node.path("Note"))).build()); + } + Map results = new HashMap<>(); + results.put(SleepDuration.SCHEMA_SLEEP_DURATION, sleepDurations); + return ShimDataResponse.result(IHealthShim.SHIM_KEY, results); + } + }), + + STEP_COUNT("activity", new JsonDeserializer() { + @Override + public ShimDataResponse deserialize(JsonParser jsonParser, DeserializationContext deserializationContext) + throws IOException { + + JsonNode responseNode = jsonParser.getCodec().readTree(jsonParser); + if (responseNode.path("ARDataList").size() == 0) { + return ShimDataResponse.empty(IHealthShim.SHIM_KEY); + } + List stepCounts = new ArrayList<>(); + for (JsonNode node : responseNode.path("ARDataList")) { + if (node.path("Steps").intValue() > 0) { + DateTime start = dateTimeValue(node.path("MDate")); + stepCounts.add(new StepCountBuilder() + .withStartAndDuration(start, 1.0, DurationUnitValue.DurationUnit.d) + .setSteps(node.path("Steps").intValue()).build()); + } + } + Map results = new HashMap<>(); + results.put(StepCount.SCHEMA_STEP_COUNT, stepCounts); + return ShimDataResponse.result(IHealthShim.SHIM_KEY, results); + } + }); + + private String endPoint; + + private JsonDeserializer normalizer; + + IHealthDataTypes(String endPoint, JsonDeserializer normalizer) { + this.endPoint = endPoint; + this.normalizer = normalizer; + } + + @Override + public JsonDeserializer getNormalizer() { + return normalizer; + } + + public String getEndPoint() { + return endPoint; + } + + private static DateTime dateTimeValue(JsonNode node) { + Preconditions.checkArgument(node.isIntegralNumber()); + return new DateTime(node.longValue() * 1000, DateTimeZone.UTC); + } + + private static String textValue(JsonNode node) { + Preconditions.checkArgument(node.isTextual()); + return Strings.emptyToNull(node.textValue().trim()); + } + } + + @Override + protected ResponseEntity getData(OAuth2RestOperations restTemplate, + ShimDataRequest shimDataRequest) throws ShimException { + + final IHealthDataTypes dataType; + try { + dataType = IHealthDataTypes.valueOf( + shimDataRequest.getDataTypeKey().trim().toUpperCase()); + } catch (NullPointerException | IllegalArgumentException e) { + throw new ShimException("Null or Invalid data type parameter: " + + shimDataRequest.getDataTypeKey() + + " in shimDataRequest, cannot retrieve data."); + } + + OAuth2AccessToken token = SerializationUtils.deserialize(shimDataRequest.getAccessParameters().getSerializedToken()); + String userId = Preconditions.checkNotNull((String) token.getAdditionalInformation().get("UserID")); + String urlRequest = API_URL + "/user/" + userId + "/" + dataType.getEndPoint() + ".json?"; + DateTime now = new DateTime(); + DateTime startDate = shimDataRequest.getStartDate() == null ? + now.minusDays(1) : shimDataRequest.getStartDate(); + DateTime endDate = shimDataRequest.getEndDate() == null ? + now.plusDays(1) : shimDataRequest.getEndDate(); + urlRequest += "&start_time=" + startDate.getMillis() / 1000; + urlRequest += "&end_time=" + endDate.getMillis() / 1000; + urlRequest += "&page_index=1"; + urlRequest += "&client_id=" + restTemplate.getResource().getClientId(); + urlRequest += "&client_secret=" + restTemplate.getResource().getClientSecret(); + urlRequest += "&access_token=" + token.getValue(); + urlRequest += "&locale=default"; + + ResponseEntity responseEntity = restTemplate.getForEntity(urlRequest, byte[].class); + + ObjectMapper objectMapper = new ObjectMapper(); + try { + if (shimDataRequest.getNormalize()) { + SimpleModule module = new SimpleModule(); + module.addDeserializer(ShimDataResponse.class, dataType.getNormalizer()); + objectMapper.registerModule(module); + return new ResponseEntity<>( + objectMapper.readValue(responseEntity.getBody(), ShimDataResponse.class), HttpStatus.OK); + } else { + return new ResponseEntity<>( + ShimDataResponse.result(IHealthShim.SHIM_KEY, objectMapper.readTree(responseEntity.getBody())), HttpStatus.OK); + } + } catch (IOException e) { + e.printStackTrace(); + throw new ShimException("Could not read response data."); + } + } + + @Override + public OAuth2ProtectedResourceDetails getResource() { + AuthorizationCodeResourceDetails resource = (AuthorizationCodeResourceDetails) super.getResource(); + resource.setAuthenticationScheme(AuthenticationScheme.none); + return resource; + } + + @Override + protected String getAuthorizationUrl(UserRedirectRequiredException exception) { + final OAuth2ProtectedResourceDetails resource = getResource(); + return exception.getRedirectUri() + + "?client_id=" + resource.getClientId() + + "&response_type=code" + + "&APIName=" + Joiner.on(' ').join(resource.getScope()) + + "&redirect_uri=" + getCallbackUrl() + "?state=" + exception.getStateKey(); + } + + public class IHealthAuthorizationCodeAccessTokenProvider extends AuthorizationCodeAccessTokenProvider { + + public IHealthAuthorizationCodeAccessTokenProvider() { + this.setTokenRequestEnhancer(new RequestEnhancer() { + + @Override + public void enhance(AccessTokenRequest request, + OAuth2ProtectedResourceDetails resource, + MultiValueMap form, HttpHeaders headers) { + + form.set("client_id", resource.getClientId()); + form.set("client_secret", resource.getClientSecret()); + form.set("redirect_uri", getCallbackUrl()); + form.set("state", request.getStateKey()); + } + }); + } + + @Override + protected HttpMethod getHttpMethod() { + return HttpMethod.GET; + } + + @Override + protected ResponseExtractor getResponseExtractor() { + return new ResponseExtractor() { + + @Override + public OAuth2AccessToken extractData(ClientHttpResponse response) throws IOException { + + JsonNode node = new ObjectMapper().readTree(response.getBody()); + String token = Preconditions.checkNotNull(node.path("AccessToken").textValue(), "Missing access token: %s", node); + String refreshToken = Preconditions.checkNotNull(node.path("RefreshToken").textValue(), "Missing refresh token: %s" + node); + String userId = Preconditions.checkNotNull(node.path("UserID").textValue(), "Missing UserID: %s", node); + long expiresIn = node.path("Expires").longValue() * 1000; + Preconditions.checkArgument(expiresIn > 0, "Missing Expires: %s", node); + + DefaultOAuth2AccessToken accessToken = new DefaultOAuth2AccessToken(token); + accessToken.setExpiration(new Date(System.currentTimeMillis() + expiresIn)); + accessToken.setRefreshToken(new DefaultOAuth2RefreshToken(refreshToken)); + accessToken.setAdditionalInformation(ImmutableMap.of("UserID", userId)); + return accessToken; + } + }; + } + } +} diff --git a/shim-server/src/main/java/org/openmhealth/shim/jawbone/Jawbone.md b/shim-server/src/main/java/org/openmhealth/shim/jawbone/Jawbone.md new file mode 100644 index 00000000..abec015e --- /dev/null +++ b/shim-server/src/main/java/org/openmhealth/shim/jawbone/Jawbone.md @@ -0,0 +1,141 @@ +### still a scratch pad for now... + +# general +docs main page: https://jawbone.com/up/developer/ + +# getting started on Jawbone + +1. sign up at https://jawbone.com/start/signup +1. create an app from https://jawbone.com/up/developer/account + 1. set the "OAuth redirect URLs" to point to your application + 1. take note of the "Client Id" and "App Secret" + +# api +api url: https://jawbone.com/nudge/api/v.1.1/users/@me +version: v.1.1 +supports retrieval based on modification date: updated_after parameter + +## authentication + +- protocol: OAuth 2.0 +- reference: https://jawbone.com/up/developer/authentication +- authorization URL: https://jawbone.com/auth/oauth2/auth +- flows + - authorization code + - https://jawbone.com/up/developer/authentication + - exchange URL: https://jawbone.com/auth/oauth2/token +- refresh token: https://jawbone.com/auth/oauth2/token + - grant_type=refresh_token +- scope: basic_read, extended_read, location_read, friends_read, mood_read, mood_write, move_read, move_write, sleep_read, sleep_write, meal_read, meal_write, weight_read, weight_write, generic_event_read, generic_event_write, heartrate_read +- supports refresh tokens: yes +- access token: Authorization: Bearer USER_ACCESS_TOKEN + - lifetime: 1 year +- redirect_uri: domain must match one of the domains specified as a redirect_uri for the API developer account of the client key/secret being used + +# pagination +- supported: yes +- default page size: 10 +- page size parameter: limit +- next page parameter: next, endpoint uri that can be appended to "https://jawbone.com/" +- previous page parameters: prev, not always available +- size gives number of entries in current page +- items are listed newest to oldest + + + +## incremental data +- endpoints accept a ‘updated_after’ parameter that retrieves items that were updated after a specific date + +## time zone and time representation +- Most data points return datetime as unix epoch nanoseconds that are aligned to UTC + +## rate limit + +- n/a + +## other +- If-Modified-Since supported, times are specified in GMT according to HTTP spec + +## endpoints +All endpoint responses contain two properties, one named ‘meta’ and one named ‘data’, both of which are JSON objects. + +### endpoint parameters +- Required parameters: None + +- Optional parameters + - date - formatted as YYYYMMDD, a single date of data for which to return + - page_token - timestamp, in unix epoch seconds, used to paginate the list of sleeps, used to refer to a specific page + - start_time - Epoch timestamp, in seconds, denoting the beginning of the time frame to retrieve, used along with end_time + - end_time - epoch timestamp, in seconds, of the end of the time frame to retrieve, used along with start_time. + - limit - the maximum number of datapoints to be retrieved + - updated_after - epoch timestamp to list events that have been updated later than the timestamp + + +### get body events +- Endpoint: /nudge/api/v.1.1/users/@me/body_events +- Reference: https://jawbone.com/up/developer/endpoints/body + +#### measures +- body weight: mapped +- bmi: mapped + +#### description +The endpoint returns different body measurements for an individual, each of which contains body measures such as weight, bmi, and body fat percentage. + +#### response +The ‘data’ property contains an array, ‘items’, that contains a list of data points (JSON objects) each of which represents a different body measurement. Each body measurement is a set of measures (e.g., body weight, body mass index, etc) taken at a specific time. + +### get moves +- endpoint: /nudge/api/v.1.1/users/@me/moves +- reference: https://jawbone.com/up/developer/endpoints/moves + +#### measures +- step count: mapped +- calories burned: not mapped, problematic representation + +#### description +Returns activity information for each day. There is only one moves entry per day, so that entry contains all of the summarized activity information for that day as well as detailed activity information within that moves datapoint. Moves data is only created through the Up app and jawbone wearables. + +#### response +The ‘data’ property contains an array, ‘items’, that contains a list of data points (JSON objects) each of which represents a single day and the activities that occurred on that day. Within each item there is summary information as well as a ‘details’ property which is a JSON object containing properties describing the activities of the day, such as ‘distance’ and ‘active_time.’ The ‘details’ object contains a dictionary, ‘hourly_totals,’ where the key is a timestamp for a given hour of that day and the value is an object with key/value pairs capturing the different activities that occurred during that hour. There is also a ‘tzs’ array that is a list of arrays representing the timezones that the user was in at different times during the hour. Each array is a timestamp paired with a timezone, representing transitions to new timezones. + +### get sleeps +- endpoint: /nudge/api/v.1.1/users/@me/sleeps +- reference: https://jawbone.com/up/developer/endpoints/sleeps + +#### description +Returns a list of sleep activities for the user, including sleep details and summary information. + +#### measures +- sleep duration: mapped + +#### response +The ‘data’ property contains an array, ‘items’, that contains a list of data points (JSON objects) each of which represents a sleep session and the information regarding that session. Each datapoint contains a ‘details’ object with more information about the sleep session including the timezone and timeslept. + +‘Duration’ is the total time spent for the sleep session, intended to be a “total time in bed” time. ‘Duration’ - ‘awake’ = the actual sleeping duration = ‘light’ + ‘sound’ (or ‘deep’) + ‘rem’ + +### get workouts +- endpoint: /nudge/api/v.1.1/users/@me/workouts +- reference: https://jawbone.com/up/developer/endpoints/workouts + +#### description +Retrieves a list of workouts that the user has completed. Workouts can be logged through Jawbone devices and applications as well as by 3rd parties. + +#### response +The ‘data’ property contains an array, ‘items’, that contains a list of data points (JSON objects) each of which represents a discrete workout activity in which the user engaged. Within each datapoint there is a ‘details’ object that contains information about the activity as well as the timezone. + +### get heartrates +- endpoint: /nudge/api/v.1.1/users/@me/heartrates +- reference: https://jawbone.com/up/developer/endpoints/heartrate + +#### measures +- heart rate: mapped + +#### description +Retrieves heart rate measurements from Jawbone specific devices. + +#### response +The ‘data’ property contains an array, ‘items’, that contains a list of data points (JSON objects) each of which represents a heart rate measurement recorded by a Jawbone device. Within each datapoint there is a ‘details’ object that contains information about the activity as well as the timezone. + + + diff --git a/shim-server/src/main/java/org/openmhealth/shim/jawbone/JawboneConfig.java b/shim-server/src/main/java/org/openmhealth/shim/jawbone/JawboneConfig.java deleted file mode 100644 index 871a27fa..00000000 --- a/shim-server/src/main/java/org/openmhealth/shim/jawbone/JawboneConfig.java +++ /dev/null @@ -1,59 +0,0 @@ -/* - * Copyright 2014 Open mHealth - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.openmhealth.shim.jawbone; - -import org.openmhealth.shim.ApplicationAccessParameters; -import org.openmhealth.shim.ApplicationAccessParametersRepo; -import org.openmhealth.shim.ShimConfig; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.context.properties.ConfigurationProperties; -import org.springframework.stereotype.Component; - -/** - * @author Danilo Bonilla - */ -@Component -@ConfigurationProperties(prefix = "openmhealth.shim.jawbone") -public class JawboneConfig implements ShimConfig { - - private String clientId; - - private String clientSecret; - - @Autowired - private ApplicationAccessParametersRepo applicationParametersRepo; - - public String getClientId() { - ApplicationAccessParameters parameters = - applicationParametersRepo.findByShimKey(JawboneShim.SHIM_KEY); - return parameters != null ? parameters.getClientId() : clientId; - } - - public void setClientId(String clientId) { - this.clientId = clientId; - } - - public String getClientSecret() { - ApplicationAccessParameters parameters = - applicationParametersRepo.findByShimKey(JawboneShim.SHIM_KEY); - return parameters != null ? parameters.getClientSecret() : clientSecret; - } - - public void setClientSecret(String clientSecret) { - this.clientSecret = clientSecret; - } -} diff --git a/shim-server/src/main/java/org/openmhealth/shim/jawbone/JawboneShim.java b/shim-server/src/main/java/org/openmhealth/shim/jawbone/JawboneShim.java index d8481703..f0f58851 100644 --- a/shim-server/src/main/java/org/openmhealth/shim/jawbone/JawboneShim.java +++ b/shim-server/src/main/java/org/openmhealth/shim/jawbone/JawboneShim.java @@ -1,5 +1,5 @@ /* - * Copyright 2014 Open mHealth + * 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. @@ -16,47 +16,44 @@ package org.openmhealth.shim.jawbone; -import com.fasterxml.jackson.core.JsonParser; -import com.fasterxml.jackson.databind.DeserializationContext; -import com.fasterxml.jackson.databind.JsonDeserializer; import com.fasterxml.jackson.databind.JsonNode; -import com.fasterxml.jackson.databind.ObjectMapper; -import com.fasterxml.jackson.databind.module.SimpleModule; -import com.jayway.jsonpath.JsonPath; -import net.minidev.json.JSONObject; -import org.joda.time.DateTime; -import org.joda.time.DateTimeZone; -import org.joda.time.format.DateTimeFormat; -import org.joda.time.format.DateTimeFormatter; -import org.openmhealth.schema.pojos.*; -import org.openmhealth.schema.pojos.build.ActivityBuilder; -import org.openmhealth.schema.pojos.build.BodyWeightBuilder; -import org.openmhealth.schema.pojos.build.SleepDurationBuilder; -import org.openmhealth.schema.pojos.build.StepCountBuilder; -import org.openmhealth.schema.pojos.generic.MassUnitValue; import org.openmhealth.shim.*; -import org.springframework.http.*; +import org.openmhealth.shim.jawbone.mapper.*; +import org.slf4j.Logger; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpMethod; +import org.springframework.http.ResponseEntity; import org.springframework.security.oauth2.client.OAuth2RestOperations; import org.springframework.security.oauth2.client.resource.OAuth2ProtectedResourceDetails; import org.springframework.security.oauth2.client.resource.UserRedirectRequiredException; import org.springframework.security.oauth2.client.token.AccessTokenRequest; import org.springframework.security.oauth2.client.token.RequestEnhancer; import org.springframework.security.oauth2.client.token.grant.code.AuthorizationCodeAccessTokenProvider; -import org.springframework.util.CollectionUtils; +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.io.IOException; -import java.util.*; +import java.time.OffsetDateTime; +import java.util.Arrays; +import java.util.List; + +import static java.util.Collections.singletonList; +import static org.slf4j.LoggerFactory.getLogger; -import static org.openmhealth.schema.pojos.generic.DurationUnitValue.DurationUnit.*; -import static org.openmhealth.schema.pojos.generic.LengthUnitValue.LengthUnit; /** - * Encapsulates parameters specific to the Jawbone API. + * Encapsulates parameters specific to the Jawbone API and processes requests for Jawbone data from shimmer. * * @author Danilo Bonilla + * @author Chris Schaefbauer */ +@Component +@ConfigurationProperties(prefix = "openmhealth.shim.jawbone") public class JawboneShim extends OAuth2ShimBase { public static final String SHIM_KEY = "jawbone"; @@ -67,18 +64,17 @@ public class JawboneShim extends OAuth2ShimBase { private static final String TOKEN_URL = "https://jawbone.com/auth/oauth2/token"; - private JawboneConfig config; + public static final List JAWBONE_SCOPES = Arrays.asList( + "extended_read", "weight_read", "heartrate_read", "meal_read", "move_read", "sleep_read"); - public static final ArrayList JAWBONE_SCOPES = - new ArrayList(Arrays.asList("extended_read", "weight_read", - "cardiac_read", "meal_read", "move_read", "sleep_read")); + private static final Logger logger = getLogger(JawboneShim.class); - public JawboneShim(AuthorizationRequestParametersRepo authorizationRequestParametersRepo, - AccessParametersRepo accessParametersRepo, - ShimServerConfig shimServerConfig1, - JawboneConfig jawboneConfig) { - super(authorizationRequestParametersRepo, accessParametersRepo, shimServerConfig1); - this.config = jawboneConfig; + @Autowired + public JawboneShim(ApplicationAccessParametersRepo applicationParametersRepo, + AuthorizationRequestParametersRepo authorizationRequestParametersRepo, + AccessParametersRepo accessParametersRepo, + ShimServerConfig shimServerConfig1) { + super(applicationParametersRepo, authorizationRequestParametersRepo, accessParametersRepo, shimServerConfig1); } @Override @@ -91,16 +87,6 @@ public String getShimKey() { return SHIM_KEY; } - @Override - public String getClientSecret() { - return config.getClientSecret(); - } - - @Override - public String getClientId() { - return config.getClientId(); - } - @Override public String getBaseAuthorizeUrl() { return AUTHORIZE_URL; @@ -120,310 +106,133 @@ public AuthorizationCodeAccessTokenProvider getAuthorizationCodeAccessTokenProvi return new JawboneAuthorizationCodeAccessTokenProvider(); } - @Override - public ShimDataRequest getTriggerDataRequest() { - ShimDataRequest shimDataRequest = new ShimDataRequest(); - shimDataRequest.setDataTypeKey(JawboneDataTypes.BODY.toString()); - shimDataRequest.setNumToReturn(1l); - return shimDataRequest; - } - @Override public ShimDataType[] getShimDataTypes() { - return new JawboneDataTypes[]{ - JawboneDataTypes.BODY, JawboneDataTypes.SLEEP, JawboneDataTypes.WORKOUTS, JawboneDataTypes.MOVES}; + + return new JawboneDataTypes[] { + JawboneDataTypes.SLEEP, JawboneDataTypes.ACTIVITY, JawboneDataTypes.BODY_MASS_INDEX, + JawboneDataTypes.WEIGHT, JawboneDataTypes.HEART_RATE, JawboneDataTypes.STEPS}; } public enum JawboneDataTypes implements ShimDataType { - BODY("body_events", new JsonDeserializer() { - @Override - public ShimDataResponse deserialize(JsonParser jsonParser, - DeserializationContext deserializationContext) - throws IOException { - JsonNode responseNode = jsonParser.getCodec().readTree(jsonParser); - String rawJson = responseNode.toString(); - - List bodyWeights = new ArrayList<>(); - - JsonPath bodyWeightsPath = JsonPath.compile("$.data.items[*]"); - List jbWeights = JsonPath.read(rawJson, bodyWeightsPath.getPath()); - if (CollectionUtils.isEmpty(jbWeights)) { - return ShimDataResponse.result(JawboneShim.SHIM_KEY, null); - } - ObjectMapper mapper = new ObjectMapper(); - for (Object rawWeight : jbWeights) { - - JsonNode jbWeight = mapper.readTree(((JSONObject) rawWeight).toJSONString()); - - DateTimeZone dateTimeZone = DateTimeZone.UTC; - if (jbWeight.get("details") != null && jbWeight.get("details").get("tz") != null) { - dateTimeZone = DateTimeZone.forID(jbWeight.get("details").get("tz") - .asText().replaceAll(" ", "_")); - } - - DateTime timeStamp = new DateTime( - jbWeight.get("time_created").asLong() * 1000, dateTimeZone); - timeStamp = timeStamp.toDateTime(DateTimeZone.UTC); - - BodyWeight bodyWeight = new BodyWeightBuilder() - .setWeight( - jbWeight.get("weight").asText(), - MassUnitValue.MassUnit.kg.toString()) - .setTimeTaken(timeStamp).build(); - - bodyWeights.add(bodyWeight); - } - Map results = new HashMap<>(); - results.put(BodyWeight.SCHEMA_BODY_WEIGHT, bodyWeights); - return ShimDataResponse.result(JawboneShim.SHIM_KEY, results); - } - }), - - SLEEP("sleeps", new JsonDeserializer() { - @Override - public ShimDataResponse deserialize(JsonParser jsonParser, - DeserializationContext deserializationContext) - throws IOException { - JsonNode responseNode = jsonParser.getCodec().readTree(jsonParser); - String rawJson = responseNode.toString(); - - List sleepDurations = new ArrayList<>(); - - JsonPath sleepsPath = JsonPath.compile("$.data.items[*]"); - List jbSleeps = JsonPath.read(rawJson, sleepsPath.getPath()); - if (CollectionUtils.isEmpty(jbSleeps)) { - return ShimDataResponse.result(JawboneShim.SHIM_KEY, null); - } - ObjectMapper mapper = new ObjectMapper(); - for (Object rawSleep : jbSleeps) { - JsonNode jbSleep = mapper.readTree(((JSONObject) rawSleep).toJSONString()); - - DateTimeZone dateTimeZone = DateTimeZone.UTC; - if (jbSleep.get("details") != null && jbSleep.get("details").get("tz") != null) { - dateTimeZone = DateTimeZone.forID(jbSleep.get("details").get("tz") - .asText().replaceAll(" ", "_")); - } - - DateTime timeStamp = new DateTime( - jbSleep.get("time_created").asLong() * 1000, dateTimeZone); - timeStamp = timeStamp.toDateTime(DateTimeZone.UTC); - - DateTime timeCompleted = new DateTime( - jbSleep.get("time_completed").asLong() * 1000, dateTimeZone); - timeCompleted = timeCompleted.toDateTime(DateTimeZone.UTC); - - SleepDuration sleepDuration = new SleepDurationBuilder() - .withStartAndEndAndDuration( - timeStamp, timeCompleted, - jbSleep.get("details").get("duration").asDouble() / 60d, - SleepDurationUnitValue.Unit.min) - .build(); - - sleepDurations.add(sleepDuration); - } - Map results = new HashMap<>(); - results.put(SleepDuration.SCHEMA_SLEEP_DURATION, sleepDurations); - return ShimDataResponse.result(JawboneShim.SHIM_KEY, results); - } - }), - - WORKOUTS("workouts", new JsonDeserializer() { - @Override - public ShimDataResponse deserialize(JsonParser jsonParser, - DeserializationContext deserializationContext) - throws IOException { - JsonNode responseNode = jsonParser.getCodec().readTree(jsonParser); - String rawJson = responseNode.toString(); - - List activities = new ArrayList<>(); - - JsonPath workoutsPath = JsonPath.compile("$.data.items[*]"); - List jbWorkouts = JsonPath.read(rawJson, workoutsPath.getPath()); - if (CollectionUtils.isEmpty(jbWorkouts)) { - return ShimDataResponse.result(JawboneShim.SHIM_KEY, null); - } - ObjectMapper mapper = new ObjectMapper(); - for (Object rawWorkout : jbWorkouts) { - JsonNode jbWorkout = mapper.readTree(((JSONObject) rawWorkout).toJSONString()); - - DateTimeZone dateTimeZone = DateTimeZone.UTC; - if (jbWorkout.get("details") != null && jbWorkout.get("details").get("tz") != null) { - dateTimeZone = DateTimeZone.forID(jbWorkout.get("details").get("tz") - .asText().replaceAll(" ", "_")); - } - - DateTime timeStamp = new DateTime( - jbWorkout.get("time_created").asLong() * 1000, dateTimeZone); - timeStamp = timeStamp.toDateTime(DateTimeZone.UTC); - - Activity activity = new ActivityBuilder() - .setActivityName(jbWorkout.get("title").asText()) - .setDistance(jbWorkout.get("details").get("meters").asDouble(), LengthUnit.m) - .withStartAndDuration( - timeStamp, jbWorkout.get("details").get("time").asDouble(), sec) - .build(); - - activities.add(activity); - } - Map results = new HashMap<>(); - results.put(Activity.SCHEMA_ACTIVITY, activities); - return ShimDataResponse.result(JawboneShim.SHIM_KEY, results); - } - }), - - @SuppressWarnings("unchecked") - MOVES("moves", new JsonDeserializer() { - @Override - public ShimDataResponse deserialize(JsonParser jsonParser, - DeserializationContext deserializationContext) - throws IOException { - - JsonNode responseNode = jsonParser.getCodec().readTree(jsonParser); - String rawJson = responseNode.toString(); - JsonPath stepsPath = JsonPath.compile("$.data.items[*]"); - - List jbStepEntries = JsonPath.read(rawJson, stepsPath.getPath()); - - if (CollectionUtils.isEmpty(jbStepEntries)) { - return ShimDataResponse.empty(JawboneShim.SHIM_KEY); - } - - DateTimeFormatter formatter = DateTimeFormat.forPattern("yyyyMMddHH"); - - List stepCounts = new ArrayList<>(); - - ObjectMapper mapper = new ObjectMapper(); - for (Object rawStepEntry : jbStepEntries) { - JsonNode jbStepEntry = mapper.readTree(((JSONObject) rawStepEntry).toJSONString()); - - DateTimeZone dateTimeZone = DateTimeZone.UTC; - if (jbStepEntry.get("details") != null && jbStepEntry.get("details").get("tz") != null) { - dateTimeZone = DateTimeZone.forID(jbStepEntry.get("details").get("tz") - .asText().replaceAll(" ", "_")); - } - - JsonNode hourlyTotals = jbStepEntry.get("details").get("hourly_totals"); - if (hourlyTotals == null) { - continue; - } - for (Iterator> iterator = hourlyTotals.fields(); iterator.hasNext(); ) { - Map.Entry item = iterator.next(); - - String timestampStr = item.getKey(); - JsonNode node = item.getValue(); - - DateTime dateTime = formatter.withZone(dateTimeZone).parseDateTime(timestampStr); - dateTime = dateTime.toDateTime(DateTimeZone.UTC); - - if (node.get("steps").asInt() > 0) { - stepCounts.add(new StepCountBuilder() - .withStartAndDuration( - dateTime, Double.parseDouble(node.get("active_time") + ""), sec) - .setSteps(node.get("steps").asInt()).build()); - } - } - } - Map results = new HashMap<>(); - results.put(StepCount.SCHEMA_STEP_COUNT, stepCounts); - return ShimDataResponse.result(JawboneShim.SHIM_KEY, results); - - } - }); + SLEEP("sleeps"), + ACTIVITY("workouts"), + WEIGHT("body_events"), + STEPS("moves"), + BODY_MASS_INDEX("body_events"), + HEART_RATE("heartrates"); private String endPoint; - private JsonDeserializer normalizer; + JawboneDataTypes(String endPoint) { - JawboneDataTypes(String endPoint, JsonDeserializer normalizer) { this.endPoint = endPoint; - this.normalizer = normalizer; - } - - @Override - public JsonDeserializer getNormalizer() { - return normalizer; } public String getEndPoint() { + return endPoint; } } protected ResponseEntity getData(OAuth2RestOperations restTemplate, - ShimDataRequest shimDataRequest) throws ShimException { - String urlRequest = DATA_URL; + ShimDataRequest shimDataRequest) throws ShimException { final JawboneDataTypes jawboneDataType; try { jawboneDataType = JawboneDataTypes.valueOf( - shimDataRequest.getDataTypeKey().trim().toUpperCase()); - } catch (NullPointerException | IllegalArgumentException e) { + shimDataRequest.getDataTypeKey().trim().toUpperCase()); + } + catch (NullPointerException | IllegalArgumentException e) { throw new ShimException("Null or Invalid data type parameter: " - + shimDataRequest.getDataTypeKey() - + " in shimDataRequest, cannot retrieve data."); + + shimDataRequest.getDataTypeKey() + + " in shimDataRequest, cannot retrieve data."); } - urlRequest += jawboneDataType.getEndPoint() + "?"; - + // FIXME this needs to get changed or documented long numToReturn = 100; if (shimDataRequest.getNumToReturn() != null) { numToReturn = shimDataRequest.getNumToReturn(); } - DateTime today = new DateTime(); + OffsetDateTime today = OffsetDateTime.now(); - DateTime startDate = shimDataRequest.getStartDate() == null ? - today.minusDays(1) : shimDataRequest.getStartDate(); - long startTimeTs = startDate.toDate().getTime() / 1000; + OffsetDateTime startDateTime = shimDataRequest.getStartDateTime() == null ? + today.minusDays(1) : shimDataRequest.getStartDateTime(); + long startTimeInEpochSecond = startDateTime.toEpochSecond(); - DateTime endDate = shimDataRequest.getEndDate() == null ? - today.plusDays(1) : shimDataRequest.getEndDate(); - long endTimeTs = endDate.toDate().getTime() / 1000; + OffsetDateTime endDateTime = shimDataRequest.getEndDateTime() == null ? + today.plusDays(1) : shimDataRequest.getEndDateTime(); + long endTimeInEpochSecond = endDateTime.toEpochSecond(); - urlRequest += "&start_time=" + startTimeTs; - urlRequest += "&end_time=" + endTimeTs; - urlRequest += "&limit=" + numToReturn; + UriComponentsBuilder uriComponentsBuilder = + UriComponentsBuilder.fromUriString(DATA_URL).path(jawboneDataType.getEndPoint()) + .queryParam("start_time", startTimeInEpochSecond).queryParam("end_time", endTimeInEpochSecond) + .queryParam("limit", numToReturn); - ObjectMapper objectMapper = new ObjectMapper(); - - ResponseEntity responseEntity = restTemplate.getForEntity(urlRequest, byte[].class); - JsonNode json = null; + ResponseEntity responseEntity; try { - if (shimDataRequest.getNormalize()) { - SimpleModule module = new SimpleModule(); - module.addDeserializer(ShimDataResponse.class, jawboneDataType.getNormalizer()); - objectMapper.registerModule(module); - return new ResponseEntity<>( - objectMapper.readValue(responseEntity.getBody(), ShimDataResponse.class), HttpStatus.OK); - } else { - return new ResponseEntity<>( - ShimDataResponse.result(JawboneShim.SHIM_KEY, objectMapper.readTree(responseEntity.getBody())), HttpStatus.OK); + responseEntity = restTemplate.getForEntity(uriComponentsBuilder.build().encode().toUri(), JsonNode.class); + } + catch (HttpClientErrorException | HttpServerErrorException e) { + // FIXME figure out how to handle this + logger.error("A request for Jawbone data failed.", e); + throw e; + } + + if (shimDataRequest.getNormalize()) { + + JawboneDataPointMapper mapper; + switch ( jawboneDataType ) { + case WEIGHT: + mapper = new JawboneBodyWeightDataPointMapper(); + break; + case STEPS: + mapper = new JawboneStepCountDataPointMapper(); + break; + case BODY_MASS_INDEX: + mapper = new JawboneBodyMassIndexDataPointMapper(); + break; + case ACTIVITY: + mapper = new JawbonePhysicalActivityDataPointMapper(); + break; + case SLEEP: + mapper = new JawboneSleepDurationDataPointMapper(); + break; + case HEART_RATE: + mapper = new JawboneHeartRateDataPointMapper(); + break; + default: + throw new UnsupportedOperationException(); } - } catch (IOException e) { - e.printStackTrace(); - throw new ShimException("Could not read response data."); + + return ResponseEntity.ok().body(ShimDataResponse + .result(JawboneShim.SHIM_KEY, mapper.asDataPoints(singletonList(responseEntity.getBody())))); + } + else { + + return ResponseEntity.ok().body(ShimDataResponse.result(JawboneShim.SHIM_KEY, responseEntity.getBody())); + } + } - protected AuthorizationRequestParameters getAuthorizationRequestParameters( - final String username, - final UserRedirectRequiredException exception) { + @Override + protected String getAuthorizationUrl(UserRedirectRequiredException exception) { + final OAuth2ProtectedResourceDetails resource = getResource(); - String authorizationUrl = exception.getRedirectUri() - + "?state=" - + exception.getStateKey() - + "&client_id=" - + resource.getClientId() - + "&response_type=code" - + "&scope=" + StringUtils.collectionToDelimitedString(resource.getScope(), " ") - + "&redirect_uri=" + getCallbackUrl(); - AuthorizationRequestParameters parameters = new AuthorizationRequestParameters(); - parameters.setRedirectUri(exception.getRedirectUri()); - parameters.setStateKey(exception.getStateKey()); - parameters.setHttpMethod(HttpMethod.GET); - parameters.setAuthorizationUrl(authorizationUrl); - return parameters; + + UriComponentsBuilder uriBuilder = UriComponentsBuilder + .fromUriString(exception.getRedirectUri()) + .queryParam("state", exception.getStateKey()) + .queryParam("client_id", resource.getClientId()) + .queryParam("response_type", "code") + .queryParam("scope", StringUtils.collectionToDelimitedString(resource.getScope(), " ")) + .queryParam("redirect_uri", getCallbackUrl()); + + return uriBuilder.build().encode().toUriString(); + } @@ -431,6 +240,7 @@ protected AuthorizationRequestParameters getAuthorizationRequestParameters( * Simple overrides to base spring class from oauth. */ public class JawboneAuthorizationCodeAccessTokenProvider extends AuthorizationCodeAccessTokenProvider { + public JawboneAuthorizationCodeAccessTokenProvider() { this.setTokenRequestEnhancer(new JawboneTokenRequestEnhancer()); } @@ -441,17 +251,18 @@ protected HttpMethod getHttpMethod() { } } + /** * Adds jawbone required parameters to authorization token requests. */ private class JawboneTokenRequestEnhancer implements RequestEnhancer { + @Override public void enhance(AccessTokenRequest request, - OAuth2ProtectedResourceDetails resource, - MultiValueMap form, HttpHeaders headers) { + OAuth2ProtectedResourceDetails resource, + MultiValueMap form, HttpHeaders headers) { form.set("client_id", resource.getClientId()); form.set("client_secret", resource.getClientSecret()); - form.set("grant_type", resource.getGrantType()); } } } diff --git a/java-schema-objects/src/main/java/org/openmhealth/schema/pojos/LabeledEnum.java b/shim-server/src/main/java/org/openmhealth/shim/jawbone/mapper/JawboneBodyEventType.java similarity index 52% rename from java-schema-objects/src/main/java/org/openmhealth/schema/pojos/LabeledEnum.java rename to shim-server/src/main/java/org/openmhealth/shim/jawbone/mapper/JawboneBodyEventType.java index 9278511a..63ed927c 100644 --- a/java-schema-objects/src/main/java/org/openmhealth/schema/pojos/LabeledEnum.java +++ b/shim-server/src/main/java/org/openmhealth/shim/jawbone/mapper/JawboneBodyEventType.java @@ -1,5 +1,5 @@ /* - * Copyright 2014 Open mHealth + * 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. @@ -14,17 +14,26 @@ * limitations under the License. */ -package org.openmhealth.schema.pojos; +package org.openmhealth.shim.jawbone.mapper; /** - * An enumeration with a non code-compliant value - * which requires special handling. - *

- * Example: enum value is 'after_exercise', label is 'After Exercise' + * Represents different body event types in Jawbone. The enum maps each type to the property name that contains its + * value. * - * @author Danilo Bonilla + * @author Chris Schaefbauer */ -public interface LabeledEnum { +public enum JawboneBodyEventType { - String getLabel(); + BODY_WEIGHT("weight"), + BODY_MASS_INDEX("bmi"); + + private String propertyName; + + JawboneBodyEventType(String propertyName) { + this.propertyName = propertyName; + } + + public String getPropertyName() { + return propertyName; + } } diff --git a/shim-server/src/main/java/org/openmhealth/shim/jawbone/mapper/JawboneBodyEventsDataPointMapper.java b/shim-server/src/main/java/org/openmhealth/shim/jawbone/mapper/JawboneBodyEventsDataPointMapper.java new file mode 100644 index 00000000..ffbfa92d --- /dev/null +++ b/shim-server/src/main/java/org/openmhealth/shim/jawbone/mapper/JawboneBodyEventsDataPointMapper.java @@ -0,0 +1,94 @@ +/* + * Copyright 2015 Open mHealth + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.openmhealth.shim.jawbone.mapper; + +import com.fasterxml.jackson.databind.JsonNode; +import org.openmhealth.schema.domain.omh.Measure; + +import java.util.Optional; + +import static org.openmhealth.schema.domain.omh.Measure.Builder; +import static org.openmhealth.shim.common.mapper.JsonNodeMappingSupport.asOptionalDouble; +import static org.openmhealth.shim.common.mapper.JsonNodeMappingSupport.asOptionalString; +import static org.openmhealth.shim.jawbone.mapper.JawboneBodyEventType.BODY_MASS_INDEX; +import static org.openmhealth.shim.jawbone.mapper.JawboneBodyEventType.BODY_WEIGHT; + + +/** + * Base class for Jawbone mappers that translate different individual body events (e.g., weight, bmi) into {@link + * Measure} objects. + * + * @author Chris Schaefbauer + * @see API documentation + */ +public abstract class JawboneBodyEventsDataPointMapper extends JawboneDataPointMapper { + + /** + * Handles some of the basic measure creation activities for body event datapoints and then delegates the creation + * of the {@link Builder} to subclasses for each individual body event {@link Measure}. + * + * @param listEntryNode an individual entry node from the "items" array of a Jawbone endpoint response + */ + @Override + protected Optional getMeasure(JsonNode listEntryNode) { + + if (!containsType(listEntryNode, getBodyEventType())) { + return Optional.empty(); + } + + Optional> builderOptional = newMeasureBuilder(listEntryNode); + if (!builderOptional.isPresent()) { + return Optional.empty(); + } + + Measure.Builder builder = builderOptional.get(); + + setEffectiveTimeFrame(builder, listEntryNode); + + Optional optionalUserNote = asOptionalString(listEntryNode, "note"); + optionalUserNote.ifPresent(builder::setUserNotes); + + return Optional.of(builder.build()); + } + + /** + * Determines whether a list entry node contains a body event of a certain type. + * + * @param listEntryNode an individual entry node from the "items" array of a Jawbone endpoint response + * @param bodyEventType the target body event type + */ + private boolean containsType(JsonNode listEntryNode, JawboneBodyEventType bodyEventType) { + + if (bodyEventType == BODY_WEIGHT || bodyEventType == BODY_MASS_INDEX) { + if (asOptionalDouble(listEntryNode, bodyEventType.getPropertyName()).isPresent()) { + return true; + } + } + + return false; + } + + /** + * Creates a {@link Builder} for a specific body event measure. + * + * @param listEntryNode an individual entry node from the "items" array of a Jawbone endpoint response + */ + abstract Optional> newMeasureBuilder(JsonNode listEntryNode); + + // TODO add Javadoc + abstract protected JawboneBodyEventType getBodyEventType(); +} diff --git a/shim-server/src/main/java/org/openmhealth/shim/jawbone/mapper/JawboneBodyMassIndexDataPointMapper.java b/shim-server/src/main/java/org/openmhealth/shim/jawbone/mapper/JawboneBodyMassIndexDataPointMapper.java new file mode 100644 index 00000000..488a53da --- /dev/null +++ b/shim-server/src/main/java/org/openmhealth/shim/jawbone/mapper/JawboneBodyMassIndexDataPointMapper.java @@ -0,0 +1,51 @@ +/* + * Copyright 2015 Open mHealth + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.openmhealth.shim.jawbone.mapper; + +import com.fasterxml.jackson.databind.JsonNode; +import org.openmhealth.schema.domain.omh.BodyMassIndex; +import org.openmhealth.schema.domain.omh.BodyMassIndexUnit; +import org.openmhealth.schema.domain.omh.Measure; +import org.openmhealth.schema.domain.omh.TypedUnitValue; + +import java.util.Optional; + +import static org.openmhealth.schema.domain.omh.BodyMassIndexUnit.KILOGRAMS_PER_SQUARE_METER; +import static org.openmhealth.shim.common.mapper.JsonNodeMappingSupport.asRequiredDouble; +import static org.openmhealth.shim.jawbone.mapper.JawboneBodyEventType.BODY_MASS_INDEX; + + +/** + * @author Chris Schaefbauer + * @see API documentation + */ +public class JawboneBodyMassIndexDataPointMapper extends JawboneBodyEventsDataPointMapper{ + + @Override + Optional> newMeasureBuilder(JsonNode listEntryNode) { + + TypedUnitValue bmiValue = + new TypedUnitValue<>(KILOGRAMS_PER_SQUARE_METER, asRequiredDouble(listEntryNode, "bmi")); + + return Optional.of(new BodyMassIndex.Builder(bmiValue)); + } + + @Override + protected JawboneBodyEventType getBodyEventType() { + return BODY_MASS_INDEX; + } +} diff --git a/shim-server/src/main/java/org/openmhealth/shim/jawbone/mapper/JawboneBodyWeightDataPointMapper.java b/shim-server/src/main/java/org/openmhealth/shim/jawbone/mapper/JawboneBodyWeightDataPointMapper.java new file mode 100644 index 00000000..4d85e3e7 --- /dev/null +++ b/shim-server/src/main/java/org/openmhealth/shim/jawbone/mapper/JawboneBodyWeightDataPointMapper.java @@ -0,0 +1,53 @@ +/* + * Copyright 2015 Open mHealth + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.openmhealth.shim.jawbone.mapper; + +import com.fasterxml.jackson.databind.JsonNode; +import org.openmhealth.schema.domain.omh.BodyWeight; +import org.openmhealth.schema.domain.omh.MassUnitValue; +import org.openmhealth.schema.domain.omh.Measure; + +import java.util.Optional; + +import static org.openmhealth.schema.domain.omh.MassUnit.KILOGRAM; +import static org.openmhealth.shim.common.mapper.JsonNodeMappingSupport.asOptionalDouble; +import static org.openmhealth.shim.jawbone.mapper.JawboneBodyEventType.BODY_WEIGHT; + + +/** + * @author Chris Schaefbauer + * @see API documentation + */ +public class JawboneBodyWeightDataPointMapper extends JawboneBodyEventsDataPointMapper { + + @Override + protected Optional> newMeasureBuilder(JsonNode listEntryNode) { + + Optional optionalWeight = asOptionalDouble(listEntryNode, "weight"); + + if (!optionalWeight.isPresent()) { + return Optional.empty(); + } + + return Optional.of(new BodyWeight.Builder(new MassUnitValue(KILOGRAM, optionalWeight.get()))); + } + + @Override + protected JawboneBodyEventType getBodyEventType() { + return BODY_WEIGHT; + } +} diff --git a/shim-server/src/main/java/org/openmhealth/shim/jawbone/mapper/JawboneDataPointMapper.java b/shim-server/src/main/java/org/openmhealth/shim/jawbone/mapper/JawboneDataPointMapper.java new file mode 100644 index 00000000..55a73047 --- /dev/null +++ b/shim-server/src/main/java/org/openmhealth/shim/jawbone/mapper/JawboneDataPointMapper.java @@ -0,0 +1,247 @@ +/* + * Copyright 2015 Open mHealth + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.openmhealth.shim.jawbone.mapper; + +import com.fasterxml.jackson.databind.JsonNode; +import org.openmhealth.schema.domain.omh.*; +import org.openmhealth.shim.common.mapper.JsonNodeDataPointMapper; + +import java.time.Instant; +import java.time.OffsetDateTime; +import java.time.ZoneId; +import java.time.ZoneOffset; +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; +import java.util.UUID; + +import static com.google.common.base.Preconditions.checkNotNull; +import static java.lang.String.format; +import static org.openmhealth.schema.domain.omh.DataPointModality.SENSED; +import static org.openmhealth.shim.common.mapper.JsonNodeMappingSupport.*; + + +/** + * The base class for mappers that translate Jawbone API responses with datapoints contained in an array to {@link + * Measure} objects. + * + * @author Chris Schaefbauer + * @author Emerson Farrugia + */ +public abstract class JawboneDataPointMapper implements JsonNodeDataPointMapper { + + public static final String RESOURCE_API_SOURCE_NAME = "Jawbone UP API"; + private static final int TIMEZONE_ENUM_INDEX_TZ = 1; + private static final int TIMEZONE_ENUM_INDEX_START = 0; + + /** + * Generates a {@link Measure} of the appropriate type from an individual list entry node with the correct values + * + * @param listEntryNode an individual entry node from the "items" array of a Jawbone endpoint response + * @return the measure mapped to from that entry, unless skipped + */ + protected abstract Optional getMeasure(JsonNode listEntryNode); + + /** + * Maps a JSON response with individual data points contained in the "items" JSON array to a list of {@link + * DataPoint} objects with the appropriate measure. Splits individual nodes and then iteratively maps the nodes in + * the list. + * + * @param responseNodes a list of a single JSON node containing the entire response from a Jawbone endpoint + * @return a list of DataPoint objects of type T with the appropriate values mapped from the input JSON; because + * these JSON objects are contained within an array in the input response, each object in that array will map into + * an item in the list + */ + @Override + public List> asDataPoints(List responseNodes) { + + // all mapped Jawbone responses only require a single endpoint response + checkNotNull(responseNodes); + checkNotNull(responseNodes.size() == 1, "A single response node is allowed per call."); + + // all mapped Jawbone responses contain a $.data.items list + JsonNode dataNode = asRequiredNode(responseNodes.get(0), "data"); + JsonNode itemsNode = asRequiredNode(dataNode, "items"); + + List> dataPoints = new ArrayList<>(); + + for (JsonNode itemNode : itemsNode) { + Optional measure = getMeasure(itemNode); + if (measure.isPresent()) { + + dataPoints.add(new DataPoint<>(getHeader(itemNode, measure.get()), measure.get())); + + } + } + + return dataPoints; + } + + /** + * @param listEntryNode an individual entry node from the "items" array of a Jawbone endpoint response + * @return a {@link DataPointHeader} for containing the appropriate information based on the input parameters + */ + protected DataPointHeader getHeader(JsonNode listEntryNode, T measure) { + + DataPointAcquisitionProvenance.Builder provenanceBuilder = + new DataPointAcquisitionProvenance.Builder(RESOURCE_API_SOURCE_NAME); + + if (isSensed(listEntryNode)) { + provenanceBuilder.setModality(SENSED); + } + + DataPointAcquisitionProvenance acquisitionProvenance = provenanceBuilder.build(); + + asOptionalString(listEntryNode, "xid") + .ifPresent(externalId -> acquisitionProvenance.setAdditionalProperty("external_id", externalId)); + // TODO discuss the name of the external identifier, to make it clear it's the ID used by the source + + asOptionalLong(listEntryNode, "time_updated").ifPresent(sourceUpdatedDateTime -> + acquisitionProvenance.setAdditionalProperty("source_updated_date_time", OffsetDateTime.ofInstant( + Instant.ofEpochSecond(sourceUpdatedDateTime), ZoneId.of("Z")))); + + DataPointHeader header = new DataPointHeader.Builder(UUID.randomUUID().toString(), measure.getSchemaId()) + .setAcquisitionProvenance(acquisitionProvenance) + .build(); + + // FIXME "shared" is never documented + asOptionalBoolean(listEntryNode, "shared") + .ifPresent(isShared -> header.setAdditionalProperty("shared", isShared)); + + return header; + } + + /** + * @param builder a {@link Measure} builder + * @param listEntryNode an individual entry node from the "items" array of a Jawbone endpoint response + */ + protected void setEffectiveTimeFrame(T.Builder builder, JsonNode listEntryNode) { + + Optional optionalStartTime = asOptionalLong(listEntryNode, "time_created"); + Optional optionalEndTime = asOptionalLong(listEntryNode, "time_completed"); + + if (optionalStartTime.isPresent() && optionalStartTime.get() != null && optionalEndTime.isPresent() && + optionalEndTime.get() != null) { + + ZoneId timeZoneForStartTime = getTimeZoneForTimestamp(listEntryNode, optionalStartTime.get()); + ZoneId timeZoneForEndTime = getTimeZoneForTimestamp(listEntryNode, optionalEndTime.get()); + + OffsetDateTime startTime = OffsetDateTime.ofInstant(Instant.ofEpochSecond(optionalStartTime.get()), + timeZoneForStartTime); + OffsetDateTime endTime = + OffsetDateTime.ofInstant(Instant.ofEpochSecond(optionalEndTime.get()), timeZoneForEndTime); + + builder.setEffectiveTimeFrame(TimeInterval.ofStartDateTimeAndEndDateTime(startTime, endTime)); + } + else if (optionalStartTime.isPresent() && optionalStartTime.get() != null) { + + ZoneId timeZoneForStartTime = getTimeZoneForTimestamp(listEntryNode, optionalStartTime.get()); + builder.setEffectiveTimeFrame( + OffsetDateTime.ofInstant(Instant.ofEpochSecond(optionalStartTime.get()), timeZoneForStartTime)); + } + } + + /** + * @param listEntryNode an individual entry node from the "items" array of a Jawbone endpoint response + * @param unixEpochTimestamp unix epoch seconds timestamp from a time property in the list entry node + * @return the appropriate {@link ZoneId} for the timestamp parameter based on the timezones contained within the + * list entry node + */ + static ZoneId getTimeZoneForTimestamp(JsonNode listEntryNode, Long unixEpochTimestamp) { + + Optional optionalTimeZonesNode = asOptionalNode(listEntryNode, "details.tzs"); + Optional optionalTimeZoneNode = asOptionalNode(listEntryNode, "details.tz"); + + ZoneId zoneIdForTimestamp = ZoneOffset.UTC; // set default to Z in case problems with getting timezone + + if (optionalTimeZonesNode.isPresent() && optionalTimeZonesNode.get().size() > 0) { + + JsonNode timeZonesNode = optionalTimeZonesNode.get(); + + if (timeZonesNode.size() == 1) { + zoneIdForTimestamp = parseZone(timeZonesNode.get(0).get(TIMEZONE_ENUM_INDEX_TZ)); + } + else { + + long currentLatestTimeZoneStart = 0; + for (JsonNode timeZoneNodesEntry : timeZonesNode) { + + long timeZoneStartTime = timeZoneNodesEntry.get(TIMEZONE_ENUM_INDEX_START).asLong(); + + if (unixEpochTimestamp >= timeZoneStartTime) { + + if (timeZoneStartTime > currentLatestTimeZoneStart) { // we cannot guarantee the order of the + // "tzs" array and we need to find the latest timezone that started before our time + + zoneIdForTimestamp = parseZone(timeZoneNodesEntry.get(TIMEZONE_ENUM_INDEX_TZ)); + currentLatestTimeZoneStart = timeZoneStartTime; + } + } + } + } + } + else if (optionalTimeZoneNode.isPresent() && !optionalTimeZoneNode.get().isNull()) { + + zoneIdForTimestamp = parseZone(optionalTimeZoneNode.get()); + } + + return zoneIdForTimestamp; + } + + // TODO clarify + /** + * Determines whether a data point is sensed. A false response does not guarantee that the data point is unsensed or + * user entered. + * + * @param listEntryNode an individual entry node from the "items" array of a Jawbone endpoint response + */ + protected boolean isSensed(JsonNode listEntryNode) { + + return false; // We make the conservative assumption that the data points are "not sensed", however + // subclasses can override based on available information within different endpoint responses or API + // documentation + } + + /** + * Translates a time zone descriptor from one of various representations (Olson, seconds offset, GMT offset) into a + * {@link ZoneId}. + * + * @param timeZoneValueNode the value associated with a timezone property + */ + static ZoneId parseZone(JsonNode timeZoneValueNode) { + + // default to UTC if timezone is not present + if (timeZoneValueNode.isNull()) { + return ZoneOffset.UTC; + } + + // "-25200" + if (timeZoneValueNode.asInt() != 0) { + ZoneOffset zoneOffset = ZoneOffset.ofTotalSeconds(timeZoneValueNode.asInt()); + + // TODO confirm if this is even necessary, since ZoneOffset is a ZoneId + return ZoneId.ofOffset("GMT", zoneOffset); + } + + // e.g., "GMT-0700" or "America/Los_Angeles" + if (timeZoneValueNode.isTextual()) { + return ZoneId.of(timeZoneValueNode.textValue()); + } + + throw new IllegalArgumentException(format("The time zone node '%s' can't be parsed.", timeZoneValueNode)); + } +} diff --git a/shim-server/src/main/java/org/openmhealth/shim/jawbone/mapper/JawboneHeartRateDataPointMapper.java b/shim-server/src/main/java/org/openmhealth/shim/jawbone/mapper/JawboneHeartRateDataPointMapper.java new file mode 100644 index 00000000..74a862a8 --- /dev/null +++ b/shim-server/src/main/java/org/openmhealth/shim/jawbone/mapper/JawboneHeartRateDataPointMapper.java @@ -0,0 +1,52 @@ +/* + * Copyright 2015 Open mHealth + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.openmhealth.shim.jawbone.mapper; + +import com.fasterxml.jackson.databind.JsonNode; +import org.openmhealth.schema.domain.omh.HeartRate; + +import java.util.Optional; + +import static org.openmhealth.schema.domain.omh.TemporalRelationshipToPhysicalActivity.AT_REST; +import static org.openmhealth.shim.common.mapper.JsonNodeMappingSupport.asRequiredLong; + + +/** + * @author Chris Schaefbauer + * @see API documentation + */ +public class JawboneHeartRateDataPointMapper extends JawboneDataPointMapper { + + @Override + protected Optional getMeasure(JsonNode listEntryNode) { + + Long restingHeartRate = asRequiredLong(listEntryNode, "resting_heartrate"); + + HeartRate.Builder heartRateBuilder = new HeartRate.Builder(restingHeartRate) + .setTemporalRelationshipToPhysicalActivity(AT_REST); + + setEffectiveTimeFrame(heartRateBuilder, listEntryNode); + + return Optional.of(heartRateBuilder.build()); + } + + @Override + protected boolean isSensed(JsonNode listEntryNode) { + // TODO add reference + return true; // Jawbone explicitly states that heart rate data only comes from their sensor-based devices + } +} diff --git a/shim-server/src/main/java/org/openmhealth/shim/jawbone/mapper/JawbonePhysicalActivityDataPointMapper.java b/shim-server/src/main/java/org/openmhealth/shim/jawbone/mapper/JawbonePhysicalActivityDataPointMapper.java new file mode 100644 index 00000000..d8c3cf3d --- /dev/null +++ b/shim-server/src/main/java/org/openmhealth/shim/jawbone/mapper/JawbonePhysicalActivityDataPointMapper.java @@ -0,0 +1,178 @@ +/* + * Copyright 2015 Open mHealth + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.openmhealth.shim.jawbone.mapper; + +import com.fasterxml.jackson.databind.JsonNode; +import org.openmhealth.schema.domain.omh.DurationUnitValue; +import org.openmhealth.schema.domain.omh.LengthUnitValue; +import org.openmhealth.schema.domain.omh.PhysicalActivity; +import org.openmhealth.schema.domain.omh.PhysicalActivity.SelfReportedIntensity; + +import javax.annotation.Nullable; +import java.time.OffsetDateTime; +import java.time.ZoneId; +import java.util.HashMap; +import java.util.Map; +import java.util.Optional; + +import static com.google.common.base.Preconditions.checkNotNull; +import static java.lang.String.format; +import static java.time.Instant.ofEpochSecond; +import static java.time.OffsetDateTime.ofInstant; +import static org.openmhealth.schema.domain.omh.DurationUnit.SECOND; +import static org.openmhealth.schema.domain.omh.LengthUnit.METER; +import static org.openmhealth.schema.domain.omh.PhysicalActivity.SelfReportedIntensity.*; +import static org.openmhealth.schema.domain.omh.TimeInterval.ofEndDateTimeAndDuration; +import static org.openmhealth.shim.common.mapper.JsonNodeMappingSupport.*; + + +/** + * A mapper from Jawbone UP API /workouts responses to {@link PhysicalActivity} objects. + * + * @author Emerson Farrugia + * @author Danilo Bonilla + * @author Chris Schaefbauer + * + * @see API documentation + */ +public class JawbonePhysicalActivityDataPointMapper extends JawboneDataPointMapper { + + private Map activityNameByWorkoutType = new HashMap<>(); + + + public JawbonePhysicalActivityDataPointMapper() { + + // initialize activity names + activityNameByWorkoutType.put(1, "walk"); + activityNameByWorkoutType.put(2, "run"); + activityNameByWorkoutType.put(3, "lift weights"); + activityNameByWorkoutType.put(4, "cross train"); + activityNameByWorkoutType.put(5, "nike training"); + activityNameByWorkoutType.put(6, "yoga"); + activityNameByWorkoutType.put(7, "pilates"); + activityNameByWorkoutType.put(8, "body weight exercise"); + activityNameByWorkoutType.put(9, "crossfit"); + activityNameByWorkoutType.put(10, "p90x"); + activityNameByWorkoutType.put(11, "zumba"); + activityNameByWorkoutType.put(12, "trx"); + activityNameByWorkoutType.put(13, "swim"); + activityNameByWorkoutType.put(14, "bike"); + activityNameByWorkoutType.put(15, "elliptical"); + activityNameByWorkoutType.put(16, "bar method"); + activityNameByWorkoutType.put(17, "kinect exercises"); + activityNameByWorkoutType.put(18, "tennis"); + activityNameByWorkoutType.put(19, "basketball"); + activityNameByWorkoutType.put(20, "golf"); + activityNameByWorkoutType.put(21, "soccer"); + activityNameByWorkoutType.put(22, "ski snowboard"); + activityNameByWorkoutType.put(23, "dance"); + activityNameByWorkoutType.put(24, "hike"); + activityNameByWorkoutType.put(25, "cross country skiing"); + activityNameByWorkoutType.put(26, "stationary bike"); + activityNameByWorkoutType.put(27, "cardio"); + activityNameByWorkoutType.put(28, "game"); + activityNameByWorkoutType.put(29, "other"); + } + + @Override + protected Optional getMeasure(JsonNode workoutNode) { + checkNotNull(workoutNode); + + // assume that the title and workout type are optional since the documentation isn't clear + Optional title = asOptionalString(workoutNode, "title"); + Optional workoutType = asOptionalInteger(workoutNode, "sub_type"); + + String activityName = getActivityName(title.orElse(null), workoutType.orElse(null)); + + PhysicalActivity.Builder builder = new PhysicalActivity.Builder(activityName); + + asOptionalInteger(workoutNode, "details.meters") + .ifPresent(distance -> builder.setDistance(new LengthUnitValue(METER, distance))); + + Optional endTimestamp = asOptionalLong(workoutNode, "time_completed"); + Optional durationInSec = asOptionalLong(workoutNode, "details.time"); + Optional timeZoneId = asOptionalZoneId(workoutNode, "details.tz"); + + if (endTimestamp.isPresent() && durationInSec.isPresent() && timeZoneId.isPresent()) { + DurationUnitValue durationUnitValue = new DurationUnitValue(SECOND, durationInSec.get()); + OffsetDateTime endDateTime = ofInstant(ofEpochSecond(endTimestamp.get()), + JawboneDataPointMapper.getTimeZoneForTimestamp(workoutNode, endTimestamp.get())); + builder.setEffectiveTimeFrame(ofEndDateTimeAndDuration(endDateTime, durationUnitValue)); + } + + asOptionalInteger(workoutNode, "details.intensity") + .ifPresent(intensity -> builder.setReportedActivityIntensity(asSelfReportedIntensity(intensity))); + + return Optional.of(builder.build()); + } + + /** + * TODO confirm that we want to make titles trump types + * + * @param title the title of the workout, if any + * @param workoutType the type of the workout, if specified + * @return the name of the activity + */ + public String getActivityName(@Nullable String title, @Nullable Integer workoutType) { + + if (title != null) { + return title; + } + + if (workoutType == null) { + return "workout"; + } + + String description = activityNameByWorkoutType.get(workoutType); + + if (description.equals("other")) { + return "workout"; + } + + return description; + } + + /** + * @param intensityValue the workout intensity value in the payload + * @return the corresponding {@link SelfReportedIntensity} + */ + public SelfReportedIntensity asSelfReportedIntensity(int intensityValue) { + + switch (intensityValue) { + case 1: + return LIGHT; + case 2: + case 3: + return MODERATE; + case 4: + case 5: + return VIGOROUS; + default: + throw new IllegalArgumentException(format("The intensity value '%d' isn't supported.", intensityValue)); + } + } + + @Override + protected boolean isSensed(JsonNode workoutNode) { + + Optional steps = asOptionalInteger(workoutNode, "details.steps"); + + // Jawbone API documentation states that steps is only included if the activity was sensed + // by a Jawbone wearable device + return steps.isPresent() && steps.get() > 0; + } +} diff --git a/shim-server/src/main/java/org/openmhealth/shim/jawbone/mapper/JawboneSleepDurationDataPointMapper.java b/shim-server/src/main/java/org/openmhealth/shim/jawbone/mapper/JawboneSleepDurationDataPointMapper.java new file mode 100644 index 00000000..90cae432 --- /dev/null +++ b/shim-server/src/main/java/org/openmhealth/shim/jawbone/mapper/JawboneSleepDurationDataPointMapper.java @@ -0,0 +1,71 @@ +/* + * Copyright 2015 Open mHealth + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.openmhealth.shim.jawbone.mapper; + +import com.fasterxml.jackson.databind.JsonNode; +import org.openmhealth.schema.domain.omh.DurationUnit; +import org.openmhealth.schema.domain.omh.DurationUnitValue; +import org.openmhealth.schema.domain.omh.SleepDuration; + +import java.util.Optional; + +import static org.openmhealth.shim.common.mapper.JsonNodeMappingSupport.asOptionalLong; +import static org.openmhealth.shim.common.mapper.JsonNodeMappingSupport.asRequiredLong; + + +/** + * @author Chris Schaefbauer + * @see API documentation + */ +public class JawboneSleepDurationDataPointMapper extends JawboneDataPointMapper { + + @Override + protected Optional getMeasure(JsonNode listEntryNode) { + + Long totalSleepSessionDuration = asRequiredLong(listEntryNode, "details.duration"); + Long totalTimeAwake = asRequiredLong(listEntryNode, "details.awake"); + + SleepDuration.Builder sleepDurationBuilder = new SleepDuration.Builder( + new DurationUnitValue(DurationUnit.SECOND, totalSleepSessionDuration - totalTimeAwake)); + + setEffectiveTimeFrame(sleepDurationBuilder, listEntryNode); + + SleepDuration sleepDuration = sleepDurationBuilder.build(); + asOptionalLong(listEntryNode, "details.awakenings") + .ifPresent(wakeUpCount -> sleepDuration.setAdditionalProperty("wakeup_count", wakeUpCount)); + + return Optional.of(sleepDuration); + } + + @Override + protected boolean isSensed(JsonNode listEntryNode) { + + Optional optionalLightSleep = asOptionalLong(listEntryNode, "details.light"); + Optional optionalAwakeTime = asOptionalLong(listEntryNode, "details.awake"); + + // Jawbone documentation states that sleep details, specifically awake and light sleep + // values, are only recorded when sleep has been sensed by a Jawbone wearable. If these values are + // zero, however, this does not guarantee that the data point is not sensed. + if (optionalAwakeTime.isPresent() && optionalLightSleep.isPresent()) { + if (optionalAwakeTime.get() > 0 || optionalLightSleep.get() > 0) { + return true; + } + } + + return false; + } +} diff --git a/shim-server/src/main/java/org/openmhealth/shim/jawbone/mapper/JawboneStepCountDataPointMapper.java b/shim-server/src/main/java/org/openmhealth/shim/jawbone/mapper/JawboneStepCountDataPointMapper.java new file mode 100644 index 00000000..7f6fa87d --- /dev/null +++ b/shim-server/src/main/java/org/openmhealth/shim/jawbone/mapper/JawboneStepCountDataPointMapper.java @@ -0,0 +1,55 @@ +/* + * Copyright 2015 Open mHealth + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.openmhealth.shim.jawbone.mapper; + +import com.fasterxml.jackson.databind.JsonNode; +import org.openmhealth.schema.domain.omh.StepCount; + +import java.util.Optional; + +import static org.openmhealth.shim.common.mapper.JsonNodeMappingSupport.asRequiredLong; + + +/** + * @author Chris Schaefbauer + */ +public class JawboneStepCountDataPointMapper extends JawboneDataPointMapper { + + @Override + protected Optional getMeasure(JsonNode listEntryNode) { + + long stepCountValue = asRequiredLong(listEntryNode, "details.steps"); + + if (stepCountValue <= 0) { + return Optional.empty(); + } + + StepCount.Builder stepCountBuilder = new StepCount.Builder(stepCountValue); + + setEffectiveTimeFrame(stepCountBuilder, listEntryNode); + + return Optional.of(stepCountBuilder.build()); + } + + @Override + protected boolean isSensed(JsonNode listEntryNode) { + + // the moves endpoint, from which step count is derived, only contains data sensed by Jawbone + // devices or by the Jawbone UP app + return true; + } +} diff --git a/shim-server/src/main/java/org/openmhealth/shim/misfit/Misfit.md b/shim-server/src/main/java/org/openmhealth/shim/misfit/Misfit.md new file mode 100644 index 00000000..9da95586 --- /dev/null +++ b/shim-server/src/main/java/org/openmhealth/shim/misfit/Misfit.md @@ -0,0 +1,89 @@ +### still a scratch pad for now... + +# general +main page: https://build.misfit.com/docs/ + +# getting started on Misfit + +1. sign up at https://build.misfit.com/signup +1. create an app https://build.misfit.com/apps + 1. set the Application Domain to wherever you will redirect your users + 1. take note of the "App Key" and "App Secret" + +# api +api url: https://api.misfitwearables.com +supports retrieval based on modification date: no + +## authentication + +- protocol: OAuth 2.0 +- https://build.misfit.com/docs/references#APIReferences-Authorize3rd-partyapptoaccessShinedata +- authorization URL: https://api.misfitwearables.com/auth/dialog/authorize +- flows + - authorization code + - https://build.misfit.com/docs/references#APIReferences-Getaccesstokenfromauthorizedcode + - exchange URL: https://api.misfitwearables.com/auth/tokens/exchange + - implicit +- scope: public,birthday,email (full list not yet supported) +- supports refresh tokens: yes/no +- access token: access_token=USER_ACCESS_TOKEN or Authorization: Bearer USER_ACCESS_TOKEN + +# pagination +- supported: no + +# rate limit + +- total: n/a +- per user: 150/hr +- limit header: X-RateLimit-Limit:150 +- remaining header: X-RateLimit-Remaining:148 +- next reset time header: X-RateLimit-Reset:1404298869 + +# endpoints + +profile +- https://build.misfit.com/docs/references#APIReferences-Profile + +device +- https://api.misfitwearables.com/move/resource/v1/user/:userId/device +- https://build.misfit.com/docs/references#APIReferences-Device +- product: + - shine + +summary +- description: daily summary +- https://api.misfitwearables.com/move/resource/v1/user/:userId/activity/summary?start_date=X&end_date=Y&detail=true +- https://build.misfit.com/docs/references#APIReferences-Summary +- limited to 31 days, 400 if longer +- supports time zone: not yet, waiting for API change +measures: + steps: mapped, but time zones are currently broken until API changes are made + calories: not mapped, pending refactor + activityCalories: not mapped, compare to sessions + distance: not mapped + +sessions +- description: workouts +- https://build.misfit.com/docs/references#APIReferences-Session +- https://api.misfitwearables.com/move/resource/v1/user/:userId/activity/sessions +- limited to 31 days, 400 if longer +- supports time zone: offset +- measures and status: + - activity: mapped + - duration: mapped + - steps: not mapped, pending refactor + - calories burned: not mapped, pending refactor + - distance: mapped + +sleep +- description: sleep information +- https://api.misfitwearables.com/move/resource/v1/user/:userId/activity/sleeps +- https://build.misfit.com/docs/references#APIReferences-Sleep +- limited to 31 days, 400 if longer +- measures: + - sleep details (awake, deep sleep, sleep): mapped, may increase granularity + +## issues + +how do we decouple summary from sessions, if summaries *contain* sessions? +still need to fix time zones in step count 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 new file mode 100644 index 00000000..a2a20857 --- /dev/null +++ b/shim-server/src/main/java/org/openmhealth/shim/misfit/MisfitShim.java @@ -0,0 +1,258 @@ +/* + * 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; + +import com.fasterxml.jackson.databind.JsonNode; +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.slf4j.Logger; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.http.HttpHeaders; +import org.springframework.http.ResponseEntity; +import org.springframework.security.oauth2.client.OAuth2RestOperations; +import org.springframework.security.oauth2.client.resource.OAuth2ProtectedResourceDetails; +import org.springframework.security.oauth2.client.resource.UserRedirectRequiredException; +import org.springframework.security.oauth2.client.token.AccessTokenRequest; +import org.springframework.security.oauth2.client.token.RequestEnhancer; +import org.springframework.security.oauth2.client.token.grant.code.AuthorizationCodeAccessTokenProvider; +import org.springframework.stereotype.Component; +import org.springframework.util.MultiValueMap; +import org.springframework.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.util.Arrays; +import java.util.List; + +import static java.util.Collections.singletonList; +import static org.slf4j.LoggerFactory.getLogger; +import static org.springframework.http.ResponseEntity.ok; + + +/** + * @author Eric Jain + * @author Emerson Farrugia + */ +@Component +@ConfigurationProperties(prefix = "openmhealth.shim.misfit") +public class MisfitShim extends OAuth2ShimBase { + + private static final Logger logger = getLogger(MisfitShim.class); + + public static final String SHIM_KEY = "misfit"; + + private static final String DATA_URL = "https://api.misfitwearables.com/move/resource/v1/user/me"; + + private static final String AUTHORIZE_URL = "https://api.misfitwearables.com/auth/dialog/authorize"; + + private static final String TOKEN_URL = "https://api.misfitwearables.com/auth/tokens/exchange"; + + public static final List MISFIT_SCOPES = Arrays.asList("public", "birthday", "email"); + + private static final long MAX_DURATION_IN_DAYS = 31; + + @Autowired + public MisfitShim(ApplicationAccessParametersRepo applicationParametersRepo, + AuthorizationRequestParametersRepo authorizationRequestParametersRepo, + AccessParametersRepo accessParametersRepo, + ShimServerConfig shimServerConfig) { + super(applicationParametersRepo, authorizationRequestParametersRepo, accessParametersRepo, shimServerConfig); + } + + private MisfitPhysicalActivityDataPointMapper physicalActivityMapper = new MisfitPhysicalActivityDataPointMapper(); + private MisfitSleepDurationDataPointMapper sleepDurationMapper = new MisfitSleepDurationDataPointMapper(); + private MisfitStepCountDataPointMapper stepCountMapper = new MisfitStepCountDataPointMapper(); + + @Override + public String getLabel() { + return "Misfit"; + } + + @Override + public String getShimKey() { + return SHIM_KEY; + } + + @Override + public String getBaseAuthorizeUrl() { + return AUTHORIZE_URL; + } + + @Override + public String getBaseTokenUrl() { + return TOKEN_URL; + } + + @Override + public List getScopes() { + return MISFIT_SCOPES; + } + + @Override + public AuthorizationCodeAccessTokenProvider getAuthorizationCodeAccessTokenProvider() { + return new MisfitAuthorizationCodeAccessTokenProvider(); + } + + @Override + public ShimDataType[] getShimDataTypes() { + return MisfitDataTypes.values(); + } + + + // TODO remove this structure once endpoints are figured out + public enum MisfitDataTypes implements ShimDataType { + + SLEEP("activity/sleeps"), + ACTIVITIES("activity/sessions"), + STEPS("activity/summary"); + + private String endPoint; + + MisfitDataTypes(String endPoint) { + this.endPoint = endPoint; + } + + public String getEndPoint() { + return endPoint; + } + } + + @Override + protected ResponseEntity getData(OAuth2RestOperations restTemplate, + ShimDataRequest shimDataRequest) throws ShimException { + + final MisfitDataTypes misfitDataType; + try { + misfitDataType = MisfitDataTypes.valueOf(shimDataRequest.getDataTypeKey().trim().toUpperCase()); + } + catch (NullPointerException | IllegalArgumentException e) { + throw new ShimException("Null or Invalid data type parameter: " + shimDataRequest.getDataTypeKey() + + " in shimDataRequest, cannot retrieve data."); + } + + // TODO don't truncate dates + OffsetDateTime now = OffsetDateTime.now(); + + OffsetDateTime startDateTime = shimDataRequest.getStartDateTime() == null ? + now.minusDays(1) : shimDataRequest.getStartDateTime(); + + OffsetDateTime endDateTime = shimDataRequest.getEndDateTime() == null ? + now.plusDays(1) : shimDataRequest.getEndDateTime(); + + if (Duration.between(startDateTime, endDateTime).toDays() > MAX_DURATION_IN_DAYS) { + endDateTime = + startDateTime.plusDays(MAX_DURATION_IN_DAYS - 1); // TODO when refactoring, break apart queries + } + + UriComponentsBuilder uriBuilder = UriComponentsBuilder + .fromUriString(DATA_URL); + + for (String pathSegment : Splitter.on("/").split(misfitDataType.getEndPoint())) { + uriBuilder.pathSegment(pathSegment); + } + + uriBuilder + .queryParam("start_date", startDateTime.toLocalDate()) // TODO convert ODT to LocalDate properly + .queryParam("end_date", endDateTime.toLocalDate()) + .queryParam("detail", true); // added to all endpoints to support summaries + + ResponseEntity responseEntity; + try { + responseEntity = restTemplate.getForEntity(uriBuilder.build().encode().toUri(), JsonNode.class); + } + catch (HttpClientErrorException | HttpServerErrorException e) { + // FIXME figure out how to handle this + logger.error("A request for Misfit data failed.", e); + throw e; + } + + if (shimDataRequest.getNormalize()) { + + MisfitDataPointMapper dataPointMapper; + + switch (misfitDataType) { + case ACTIVITIES: + dataPointMapper = physicalActivityMapper; + break; + case SLEEP: + dataPointMapper = sleepDurationMapper; + break; + case STEPS: + dataPointMapper = stepCountMapper; + break; + default: + throw new UnsupportedOperationException(); + } + + return ok().body(ShimDataResponse.result(SHIM_KEY, + dataPointMapper.asDataPoints(singletonList(responseEntity.getBody())))); + } + else { + return ok().body(ShimDataResponse.result(SHIM_KEY, responseEntity.getBody())); + } + } + + @Override + protected String getAuthorizationUrl(UserRedirectRequiredException exception) { + + final OAuth2ProtectedResourceDetails resource = getResource(); + + UriComponentsBuilder uriBuilder = UriComponentsBuilder + .fromUriString(exception.getRedirectUri()) + .queryParam("state", exception.getStateKey()) + .queryParam("client_id", resource.getClientId()) + .queryParam("response_type", "code") + .queryParam("scope", Joiner.on(',').join(resource.getScope())) + .queryParam("redirect_uri", getCallbackUrl()); + + return uriBuilder.build().encode().toUriString(); + } + + /** + * Simple overrides to base spring class from oauth. + */ + public class MisfitAuthorizationCodeAccessTokenProvider extends AuthorizationCodeAccessTokenProvider { + + public MisfitAuthorizationCodeAccessTokenProvider() { + this.setTokenRequestEnhancer(new MisfitTokenRequestEnhancer()); + } + } + + + /** + * Adds jawbone required parameters to authorization token requests. + */ + private class MisfitTokenRequestEnhancer implements RequestEnhancer { + + @Override + public void enhance(AccessTokenRequest request, OAuth2ProtectedResourceDetails resource, + MultiValueMap form, HttpHeaders headers) { + + form.set("client_id", resource.getClientId()); + form.set("client_secret", resource.getClientSecret()); + form.set("redirect_uri", getCallbackUrl()); + } + } +} diff --git a/shim-server/src/main/java/org/openmhealth/shim/misfit/mapper/MisfitDataPointMapper.java b/shim-server/src/main/java/org/openmhealth/shim/misfit/mapper/MisfitDataPointMapper.java new file mode 100644 index 00000000..3e1c2e4e --- /dev/null +++ b/shim-server/src/main/java/org/openmhealth/shim/misfit/mapper/MisfitDataPointMapper.java @@ -0,0 +1,105 @@ +/* + * 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.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.util.ArrayList; +import java.util.List; +import java.util.Optional; +import java.util.UUID; + +import static com.google.common.base.Preconditions.checkNotNull; +import static org.openmhealth.schema.domain.omh.DataPointModality.SENSED; +import static org.openmhealth.shim.common.mapper.JsonNodeMappingSupport.asRequiredNode; + + +/** + * @author Emerson Farrugia + */ +public abstract class MisfitDataPointMapper implements JsonNodeDataPointMapper { + + public static final String RESOURCE_API_SOURCE_NAME = "Misfit Resource API"; + + @Override + public List> asDataPoints(List responseNodes) { + + // all mapped Misfit responses only require a single endpoint response + checkNotNull(responseNodes); + checkNotNull(responseNodes.size() == 1, "A single response node is allowed per call."); + + // all mapped Misfit responses contain a single list + JsonNode listNode = asRequiredNode(responseNodes.get(0), getListNodeName()); + + List> dataPoints = new ArrayList<>(); + + for (JsonNode listEntryNode : listNode) { + asDataPoint(listEntryNode).ifPresent(dataPoints::add); + } + + return dataPoints; + } + + /** + * @return the name of the list node used by this mapper + */ + protected abstract String getListNodeName(); + + /** + * @param listEntryNode the list entry node + * @return the data point mapped to from that entry, unless skipped + */ + protected abstract Optional> asDataPoint(JsonNode listEntryNode); + + + /** + * @param measure the body of the data point + * @param sourceName the name of the source of the measure + * @param externalId the identifier of the measure as recorded by the data provider + * @param sensed true if the measure is sensed by a device, false if it's manually entered, null otherwise + * @param the measure type + * @return a data point + */ + protected DataPoint newDataPoint(T measure, String sourceName, String externalId, + Boolean sensed) { + + DataPointAcquisitionProvenance.Builder provenanceBuilder = + new DataPointAcquisitionProvenance.Builder(sourceName); + + if (sensed != null && sensed) { + provenanceBuilder.setModality(SENSED); + } + + DataPointAcquisitionProvenance acquisitionProvenance = provenanceBuilder.build(); + + // TODO discuss the name of the external identifier, to make it clear it's the ID used by the source + if (externalId != null) { + acquisitionProvenance.setAdditionalProperty("external_id", externalId); + } + + DataPointHeader header = new DataPointHeader.Builder(UUID.randomUUID().toString(), measure.getSchemaId()) + .setAcquisitionProvenance(acquisitionProvenance) + .build(); + + return new DataPoint<>(header, measure); + } +} 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 new file mode 100644 index 00000000..950b13b9 --- /dev/null +++ b/shim-server/src/main/java/org/openmhealth/shim/misfit/mapper/MisfitPhysicalActivityDataPointMapper.java @@ -0,0 +1,78 @@ +/* + * 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.DataPoint; +import org.openmhealth.schema.domain.omh.DurationUnitValue; +import org.openmhealth.schema.domain.omh.LengthUnitValue; +import org.openmhealth.schema.domain.omh.PhysicalActivity; + +import java.time.OffsetDateTime; +import java.util.Optional; + +import static com.google.common.base.Preconditions.checkNotNull; +import static org.openmhealth.schema.domain.omh.DurationUnit.SECOND; +import static org.openmhealth.schema.domain.omh.LengthUnit.MILE; +import static org.openmhealth.schema.domain.omh.TimeInterval.ofStartDateTimeAndDuration; +import static org.openmhealth.shim.common.mapper.JsonNodeMappingSupport.*; + + +/** + * A mapper from Misfit Resource API /activity/sessions responses to {@link PhysicalActivity} objects. + * + * @author Emerson Farrugia + * @author Eric Jain + * @see API documentation + */ +public class MisfitPhysicalActivityDataPointMapper extends MisfitDataPointMapper { + + @Override + protected String getListNodeName() { + return "sessions"; + } + + @Override + public Optional> asDataPoint(JsonNode sessionNode) { + + checkNotNull(sessionNode); + + String activityName = asRequiredString(sessionNode, "activityType"); + + PhysicalActivity.Builder builder = new PhysicalActivity.Builder(activityName); + + Optional distance = asOptionalDouble(sessionNode, "distance"); + + if (distance.isPresent()) { + builder.setDistance(new LengthUnitValue(MILE, distance.get())); + } + + Optional startDateTime = asOptionalOffsetDateTime(sessionNode, "startTime"); + Optional durationInSec = asOptionalDouble(sessionNode, "duration"); + + if (startDateTime.isPresent() && durationInSec.isPresent()) { + DurationUnitValue durationUnitValue = new DurationUnitValue(SECOND, durationInSec.get()); + builder.setEffectiveTimeFrame(ofStartDateTimeAndDuration(startDateTime.get(), durationUnitValue)); + } + + PhysicalActivity measure = builder.build(); + + Optional externalId = asOptionalString(sessionNode, "id"); + + return Optional.of(newDataPoint(measure, RESOURCE_API_SOURCE_NAME, externalId.orElse(null), null)); + } +} 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 new file mode 100644 index 00000000..48597d26 --- /dev/null +++ b/shim-server/src/main/java/org/openmhealth/shim/misfit/mapper/MisfitSleepDurationDataPointMapper.java @@ -0,0 +1,110 @@ +/* + * Copyright 2015 Open mHealth + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.openmhealth.shim.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.SleepDuration; +import org.openmhealth.shim.common.mapper.JsonNodeMappingException; + +import java.time.Duration; +import java.time.OffsetDateTime; +import java.util.Optional; + +import static java.lang.String.format; +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 Misfit Resource API /activity/sleeps responses to {@link SleepDuration} 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. + * + * @author Emerson Farrugia + * @see API documentation + */ +public class MisfitSleepDurationDataPointMapper extends MisfitDataPointMapper { + + public static final int AWAKE_SEGMENT_TYPE = 1; + + @Override + protected String getListNodeName() { + return "sleeps"; + } + + @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; + + OffsetDateTime previousSegmentStartDateTime = null; + Long previousSegmentType = null; + + for (JsonNode sleepDetailSegmentNode : sleepDetailsNode) { + + 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; + } + + // 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)); + } + + // 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(); + } + + SleepDuration measure = new SleepDuration.Builder(new DurationUnitValue(SECOND, sleepDurationInSec)) + .setEffectiveTimeFrame(ofStartDateTimeAndEndDateTime(startDateTime, endDateTime)) + .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)); + } +} 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 new file mode 100644 index 00000000..8812e663 --- /dev/null +++ b/shim-server/src/main/java/org/openmhealth/shim/misfit/mapper/MisfitStepCountDataPointMapper.java @@ -0,0 +1,76 @@ +/* + * 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.DataPoint; +import org.openmhealth.schema.domain.omh.DurationUnitValue; +import org.openmhealth.schema.domain.omh.StepCount; + +import java.time.LocalDate; +import java.time.OffsetDateTime; +import java.util.Optional; + +import static com.google.common.base.Preconditions.checkNotNull; +import static java.time.ZoneOffset.UTC; +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; +import static org.openmhealth.shim.common.mapper.JsonNodeMappingSupport.asRequiredLong; + + +/** + * A mapper from Misfit Resource API /activity/summary?detail=true responses to {@link StepCount} objects. + * + * @author Emerson Farrugia + * @author Eric Jain + * @see API documentation + */ +public class MisfitStepCountDataPointMapper extends MisfitDataPointMapper { + + @Override + protected String getListNodeName() { + return "summary"; + } + + @Override + public Optional> asDataPoint(JsonNode summaryNode) { + + checkNotNull(summaryNode); + + Long stepCount = asRequiredLong(summaryNode, "steps"); + + if (stepCount == 0) { + return Optional.empty(); + } + + StepCount.Builder builder = new StepCount.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)); + + StepCount measure = builder.build(); + + return Optional.of(newDataPoint(measure, RESOURCE_API_SOURCE_NAME, null, null)); + } +} diff --git a/shim-server/src/main/java/org/openmhealth/shim/runkeeper/RunKeeper.md b/shim-server/src/main/java/org/openmhealth/shim/runkeeper/RunKeeper.md new file mode 100644 index 00000000..e394ef48 --- /dev/null +++ b/shim-server/src/main/java/org/openmhealth/shim/runkeeper/RunKeeper.md @@ -0,0 +1,79 @@ +### still a scratch pad for now... + +# general +docs main page: https://http://developer.runkeeper.com/healthgraph/overview/ + +# getting started on RunKeeper + +1. sign up at http://runkeeper.com/partner +1. create an app http://runkeeper.com/partner/applications/view + 1. check the Read Health Information and Retain Health Information permission requests + 1. click Keys and URLs and take note of the "Client ID" and "Client Secret" + +# api +api url: https://api.runkeeper.com +supports retrieval based on modification date: modifiedNoEarlierThan and modifiedNoLaterThan query parameters + +## authentication + +- protocol: OAuth 2.0 +- http://runkeeper.com/developer/healthgraph/registration-authorization +- authorization URL: https://runkeeper.com/apps/authorize +- deauthorization URL: https://runkeeper.com/apps/de-authorize +- flows + - authorization code + - reference: http://runkeeper.com/developer/healthgraph/registration-authorization + - exchange URL: https://runkeeper.com/apps/token +- scope: does not support scopes +- supports refresh tokens: no, access tokens do not expire +- access token: access_token=USER_ACCESS_TOKEN or Authorization: Bearer USER_ACCESS_TOKEN + +# pagination +- supported: by feed endpoints +- page size defaults to 25, overridden using pageSize query parameter +- size gives total number of entries across all pages +- items are listed newest to oldest +- 'next' property in the response body, if it exists, provides the endpoint for the next page +- 'previous' property in the response body, if it exists, provides the endpoint for the previous page + +# time zone and time representation +- RunKeeper sourced datapoints contain a utc_offset field +- API does not allow third parties to provide time zone information meaning that any data provided from a third party will not have timezone information + +# other +- If-Modified-Since supported, times are specified in GMT according to HTTP spec + +# rate limit + +- n/a + +# endpoints + +## time frame parameters +- noEarlierThan +- noLaterThan +- values: take dates in the form ‘YYYY-MM-DD’ and are inclusive, meaning that they will include the dates that +are provided as the parameters and exclude items on days that are on the next day in the case of noLaterThan or on the +previous day in the case of noEarlierThan + +## get fitness activities +- uri: /fitnessActivities +- description: workouts +- datatype: application/vnd.com.runkeeper.FitnessActivity+json +- reference: http://developer.runkeeper.com/healthgraph/fitness-activities +- supports time zone: utc offset for runkeeper datapoints only, but not fractional +measures: + physical activity: mapped + calories burned: mapped + heart rate: not mapped, not retrieved from the activity history (list of all activity datapoints) + +## get weight +- uri: /weight +- datatype: application/vnd.com.runkeeper.WeightSetFeed+json +- description: weight +- http://runkeeper.com/developer/healthgraph/weight-sets +- supports time zone: no +measures: + body weight: not mapped because of time zones + +## issues diff --git a/shim-server/src/main/java/org/openmhealth/shim/runkeeper/RunkeeperConfig.java b/shim-server/src/main/java/org/openmhealth/shim/runkeeper/RunkeeperConfig.java deleted file mode 100644 index f485acfd..00000000 --- a/shim-server/src/main/java/org/openmhealth/shim/runkeeper/RunkeeperConfig.java +++ /dev/null @@ -1,59 +0,0 @@ -/* - * Copyright 2014 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.runkeeper; - -import org.openmhealth.shim.ApplicationAccessParameters; -import org.openmhealth.shim.ApplicationAccessParametersRepo; -import org.openmhealth.shim.ShimConfig; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.context.properties.ConfigurationProperties; -import org.springframework.stereotype.Component; - -/** - * @author Danilo Bonilla - */ -@Component -@ConfigurationProperties(prefix = "openmhealth.shim.runkeeper") -public class RunkeeperConfig implements ShimConfig { - - private String clientId; - - private String clientSecret; - - @Autowired - private ApplicationAccessParametersRepo applicationParametersRepo; - - public String getClientId() { - ApplicationAccessParameters parameters = - applicationParametersRepo.findByShimKey(RunkeeperShim.SHIM_KEY); - return parameters != null ? parameters.getClientId() : clientId; - } - - public void setClientId(String clientId) { - this.clientId = clientId; - } - - public String getClientSecret() { - ApplicationAccessParameters parameters = - applicationParametersRepo.findByShimKey(RunkeeperShim.SHIM_KEY); - return parameters != null ? parameters.getClientSecret() : clientSecret; - } - - public void setClientSecret(String clientSecret) { - this.clientSecret = clientSecret; - } -} diff --git a/shim-server/src/main/java/org/openmhealth/shim/runkeeper/RunkeeperShim.java b/shim-server/src/main/java/org/openmhealth/shim/runkeeper/RunkeeperShim.java index 33a3a738..225b989d 100644 --- a/shim-server/src/main/java/org/openmhealth/shim/runkeeper/RunkeeperShim.java +++ b/shim-server/src/main/java/org/openmhealth/shim/runkeeper/RunkeeperShim.java @@ -1,11 +1,11 @@ /* - * Copyright 2014 Open mHealth + * 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 + * 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, @@ -16,47 +16,50 @@ package org.openmhealth.shim.runkeeper; -import com.fasterxml.jackson.core.JsonParser; -import com.fasterxml.jackson.databind.DeserializationContext; -import com.fasterxml.jackson.databind.JsonDeserializer; import com.fasterxml.jackson.databind.JsonNode; -import com.fasterxml.jackson.databind.ObjectMapper; -import com.fasterxml.jackson.databind.module.SimpleModule; -import com.jayway.jsonpath.JsonPath; -import net.minidev.json.JSONObject; -import org.joda.time.DateTime; -import org.joda.time.DateTimeZone; -import org.joda.time.format.DateTimeFormat; -import org.joda.time.format.DateTimeFormatter; -import org.openmhealth.schema.pojos.Activity; -import org.openmhealth.schema.pojos.BodyWeight; -import org.openmhealth.schema.pojos.build.ActivityBuilder; -import org.openmhealth.schema.pojos.build.BodyWeightBuilder; -import org.openmhealth.schema.pojos.generic.DurationUnitValue; -import org.openmhealth.schema.pojos.generic.MassUnitValue; import org.openmhealth.shim.*; -import org.springframework.http.*; +import org.openmhealth.shim.runkeeper.mapper.RunkeeperCaloriesBurnedDataPointMapper; +import org.openmhealth.shim.runkeeper.mapper.RunkeeperDataPointMapper; +import org.openmhealth.shim.runkeeper.mapper.RunkeeperPhysicalActivityDataPointMapper; +import org.slf4j.Logger; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.http.HttpEntity; +import org.springframework.http.HttpHeaders; +import org.springframework.http.ResponseEntity; import org.springframework.security.oauth2.client.OAuth2RestOperations; import org.springframework.security.oauth2.client.resource.OAuth2ProtectedResourceDetails; import org.springframework.security.oauth2.client.resource.UserRedirectRequiredException; import org.springframework.security.oauth2.client.token.AccessTokenRequest; import org.springframework.security.oauth2.client.token.RequestEnhancer; import org.springframework.security.oauth2.client.token.grant.code.AuthorizationCodeAccessTokenProvider; -import org.springframework.util.CollectionUtils; +import org.springframework.stereotype.Component; import org.springframework.util.MultiValueMap; +import org.springframework.web.client.HttpClientErrorException; +import org.springframework.web.client.HttpServerErrorException; +import org.springframework.web.util.UriComponentsBuilder; -import java.io.IOException; -import java.util.*; +import java.time.OffsetDateTime; +import java.util.Arrays; +import java.util.List; + +import static java.util.Collections.singletonList; +import static org.slf4j.LoggerFactory.getLogger; +import static org.springframework.http.HttpMethod.GET; +import static org.springframework.http.ResponseEntity.ok; -import static org.openmhealth.schema.pojos.generic.LengthUnitValue.LengthUnit.m; /** - * Encapsulates parameters specific to jawbone api. - * * @author Danilo Bonilla + * @author Emerson Farrugia + * @author Chris Schaefbauer */ +@Component +@ConfigurationProperties(prefix = "openmhealth.shim.runkeeper") public class RunkeeperShim extends OAuth2ShimBase { + private static final Logger logger = getLogger(RunkeeperShim.class); + public static final String SHIM_KEY = "runkeeper"; private static final String DATA_URL = "https://api.runkeeper.com"; @@ -65,19 +68,15 @@ public class RunkeeperShim extends OAuth2ShimBase { private static final String TOKEN_URL = "https://runkeeper.com/apps/token"; - private RunkeeperConfig config; + public static final List RUNKEEPER_SCOPES = Arrays.asList( + "application/vnd.com.runkeeper.FitnessActivityFeed+json"); - public static final ArrayList RUNKEEPER_SCOPES = - new ArrayList(Arrays.asList( - "application/vnd.com.runkeeper.FitnessActivityFeed+json" - )); - - public RunkeeperShim(AuthorizationRequestParametersRepo authorizationRequestParametersRepo, - AccessParametersRepo accessParametersRepo, - ShimServerConfig shimServerConfig1, - RunkeeperConfig runkeeperConfig) { - super(authorizationRequestParametersRepo, accessParametersRepo, shimServerConfig1); - this.config = runkeeperConfig; + @Autowired + public RunkeeperShim(ApplicationAccessParametersRepo applicationParametersRepo, + AuthorizationRequestParametersRepo authorizationRequestParametersRepo, + AccessParametersRepo accessParametersRepo, + ShimServerConfig shimServerConfig1) { + super(applicationParametersRepo, authorizationRequestParametersRepo, accessParametersRepo, shimServerConfig1); } @Override @@ -90,16 +89,6 @@ public String getShimKey() { return SHIM_KEY; } - @Override - public String getClientSecret() { - return config.getClientSecret(); - } - - @Override - public String getClientId() { - return config.getClientId(); - } - @Override public String getBaseAuthorizeUrl() { return AUTHORIZE_URL; @@ -115,149 +104,31 @@ public List getScopes() { return RUNKEEPER_SCOPES; } - //Example: Wed, 6 Aug 2014 04:49:00 - private static DateTimeFormatter dateFormatter = DateTimeFormat.forPattern("EEE, d MMM yyyy HH:mm:ss"); + @Override + public AuthorizationCodeAccessTokenProvider getAuthorizationCodeAccessTokenProvider() { + AuthorizationCodeAccessTokenProvider provider = new AuthorizationCodeAccessTokenProvider(); + provider.setTokenRequestEnhancer(new RunkeeperTokenRequestEnhancer()); + return provider; + } + @Override + public ShimDataType[] getShimDataTypes() { + return RunkeeperDataType.values(); + } + + + // TODO remove this structure once endpoints are figured out public enum RunkeeperDataType implements ShimDataType { - ACTIVITY("application/vnd.com.runkeeper.FitnessActivityFeed+json", - "fitnessActivities", - new JsonDeserializer() { - @Override - public ShimDataResponse deserialize(JsonParser jsonParser, - DeserializationContext ctxt) - throws IOException { - JsonNode responseNode = jsonParser.getCodec().readTree(jsonParser); - String rawJson = responseNode.toString(); - - List activities = new ArrayList<>(); - - JsonPath activityPath = JsonPath.compile("$.items[*]"); - - final List rkActivities = JsonPath.read(rawJson, activityPath.getPath()); - if (CollectionUtils.isEmpty(rkActivities)) { - return ShimDataResponse.result(RunkeeperShim.SHIM_KEY, null); - } - - ObjectMapper mapper = new ObjectMapper(); - for (Object fva : rkActivities) { - final JsonNode rkActivity = mapper.readTree(((JSONObject) fva).toJSONString()); - - - Integer utcOffset = rkActivity.get("utc_offset") != null ? - rkActivity.get("utc_offset").asInt() : 0; - - DateTime startTime = - dateFormatter.withZone( - utcOffset == 0 ? DateTimeZone.UTC : - DateTimeZone.forOffsetHours(utcOffset)) - .parseDateTime(rkActivity.get("start_time").asText()); - - Activity activity = new ActivityBuilder() - .setActivityName(rkActivity.get("type").asText()) - .setDistance( - rkActivity.get("total_distance").asDouble(), m) - .withStartAndDuration( - startTime, - rkActivity.get("duration").asDouble(), - DurationUnitValue.DurationUnit.sec).build(); - - activities.add(activity); - } - Map results = new HashMap<>(); - results.put(Activity.SCHEMA_ACTIVITY, activities); - return ShimDataResponse.result(RunkeeperShim.SHIM_KEY, results); - } - }), - - GENERAL_MEASUREMENT( - "application/vnd.com.runkeeper.GeneralMeasurementSetFeed+json", - "generalMeasurements", new JsonDeserializer() { - @Override - public ShimDataResponse deserialize( - JsonParser jsonParser, DeserializationContext deserializationContext) - throws IOException { - return ShimDataResponse.empty(RunkeeperShim.SHIM_KEY); - } - }), - - DIABETES( - "application/vnd.com.runkeeper.DiabetesMeasurementSet+json", - "diabetes", new JsonDeserializer() { - @Override - public ShimDataResponse deserialize( - JsonParser jsonParser, DeserializationContext deserializationContext) - throws IOException { - return ShimDataResponse.empty(RunkeeperShim.SHIM_KEY); - } - }), - - SLEEP( - "application/vnd.com.runkeeper.SleepSetFeed+json", - "sleep", new JsonDeserializer() { - @Override - public ShimDataResponse deserialize( - JsonParser jsonParser, DeserializationContext deserializationContext) - throws IOException { - return ShimDataResponse.empty(RunkeeperShim.SHIM_KEY); - } - }), - - WEIGHT( - "application/vnd.com.runkeeper.WeightSetFeed+json", - "weight", - new JsonDeserializer() { - @Override - public ShimDataResponse deserialize(JsonParser jsonParser, DeserializationContext ctxt) - throws IOException { - JsonNode responseNode = jsonParser.getCodec().readTree(jsonParser); - String rawJson = responseNode.toString(); - - List bodyWeights = new ArrayList<>(); - JsonPath bodyWeightsPath = JsonPath.compile("$.items[*]"); - - List rkWeights = JsonPath.read(rawJson, bodyWeightsPath.getPath()); - if (CollectionUtils.isEmpty(rkWeights)) { - return ShimDataResponse.result(RunkeeperShim.SHIM_KEY, null); - } - ObjectMapper mapper = new ObjectMapper(); - for (Object fva : rkWeights) { - JsonNode rkWeight = mapper.readTree(((JSONObject) fva).toJSONString()); - - DateTime timeStamp = - dateFormatter.withZone(DateTimeZone.UTC) - .parseDateTime(rkWeight.get("timestamp").asText()); - - BodyWeight bodyWeight = new BodyWeightBuilder() - .setWeight( - rkWeight.get("weight").asText(), - MassUnitValue.MassUnit.kg.toString()) - .setTimeTaken(timeStamp).build(); - - bodyWeights.add(bodyWeight); - } - Map results = new HashMap<>(); - results.put(BodyWeight.SCHEMA_BODY_WEIGHT, bodyWeights); - return ShimDataResponse.result(RunkeeperShim.SHIM_KEY, results); - } - }); + ACTIVITY("application/vnd.com.runkeeper.FitnessActivityFeed+json", "fitnessActivities"), + CALORIES("application/vnd.com.runkeeper.FitnessActivityFeed+json", "fitnessActivities"); private String dataTypeHeader; - private String endPointUrl; - private JsonDeserializer normalizer; - - RunkeeperDataType(String dataTypeHeader, String endPointUrl, - JsonDeserializer normalizer) { + RunkeeperDataType(String dataTypeHeader, String endPointUrl) { this.dataTypeHeader = dataTypeHeader; this.endPointUrl = endPointUrl; - this.normalizer = normalizer; - } - - @Override - public JsonDeserializer getNormalizer() { - return normalizer; } public String getDataTypeHeader() { @@ -269,126 +140,102 @@ public String getEndPointUrl() { } } - - public AuthorizationCodeAccessTokenProvider getAuthorizationCodeAccessTokenProvider() { - AuthorizationCodeAccessTokenProvider provider = new AuthorizationCodeAccessTokenProvider(); - provider.setTokenRequestEnhancer(new RunkeeperTokenRequestEnhancer()); - return provider; - } - - @Override - public ShimDataRequest getTriggerDataRequest() { - ShimDataRequest shimDataRequest = new ShimDataRequest(); - shimDataRequest.setDataTypeKey(RunkeeperDataType.ACTIVITY.toString()); - shimDataRequest.setNumToReturn(1l); - return shimDataRequest; - } - - @Override - public ShimDataType[] getShimDataTypes() { - return RunkeeperDataType.values(); - } - protected ResponseEntity getData(OAuth2RestOperations restTemplate, - ShimDataRequest shimDataRequest) throws ShimException { + ShimDataRequest shimDataRequest) throws ShimException { String dataTypeKey = shimDataRequest.getDataTypeKey().trim().toUpperCase(); RunkeeperDataType runkeeperDataType; try { runkeeperDataType = RunkeeperDataType.valueOf(dataTypeKey); - } catch (NullPointerException | IllegalArgumentException e) { - throw new ShimException("Null or Invalid data type parameter: " - + dataTypeKey + " in shimDataRequest, cannot retrieve data."); } - - String urlRequest = DATA_URL; - urlRequest += "/" + runkeeperDataType.getEndPointUrl(); - - HttpHeaders headers = new HttpHeaders(); - headers.set("Accept", runkeeperDataType.getDataTypeHeader()); - - final DateTimeFormatter formatter = DateTimeFormat.forPattern("yyyy-MM-dd"); + catch (NullPointerException | IllegalArgumentException e) { + throw new ShimException("Null or Invalid data type parameter: " + dataTypeKey + + " in shimDataRequest, cannot retrieve data."); + } /*** * Setup default date parameters */ - DateTime today = new DateTime(); + OffsetDateTime now = OffsetDateTime.now(); - DateTime startDate = shimDataRequest.getStartDate() == null ? - today.minusDays(1) : shimDataRequest.getStartDate(); - String dateStart = startDate.toString(formatter); + OffsetDateTime startDateTime = shimDataRequest.getStartDateTime() == null ? + now.minusDays(1) : shimDataRequest.getStartDateTime(); - DateTime endDate = shimDataRequest.getEndDate() == null ? - today.plusDays(1) : shimDataRequest.getEndDate(); - String dateEnd = endDate.toString(formatter); + OffsetDateTime endDateTime = shimDataRequest.getEndDateTime() == null ? + now.plusDays(1) : shimDataRequest.getEndDateTime(); long numToReturn = shimDataRequest.getNumToReturn() == null || - shimDataRequest.getNumToReturn() <= 0 ? 100 : - shimDataRequest.getNumToReturn(); - - String urlParams = ""; - - urlParams += "&noEarlierThan=" + dateStart; - urlParams += "&noLaterThan=" + dateEnd; - urlParams += "&pageSize=" + numToReturn; + shimDataRequest.getNumToReturn() <= 0 ? 100 : + shimDataRequest.getNumToReturn(); - urlRequest += "".equals(urlParams) ? - "" : ("?" + urlParams.substring(1, urlParams.length())); + UriComponentsBuilder uriBuilder = UriComponentsBuilder + .fromUriString(DATA_URL) + .pathSegment(runkeeperDataType.getEndPointUrl()) + .queryParam("noEarlierThan", startDateTime.toLocalDate()) + .queryParam("noLaterThan", endDateTime.toLocalDate()) + .queryParam("pageSize", numToReturn) + .queryParam("detail", true); // added to all endpoints to support summaries - ObjectMapper objectMapper = new ObjectMapper(); - ResponseEntity response = restTemplate.exchange( - urlRequest, - HttpMethod.GET, - new HttpEntity(headers), - byte[].class); + HttpHeaders headers = new HttpHeaders(); + headers.set("Accept", runkeeperDataType.getDataTypeHeader()); + ResponseEntity responseEntity; try { - if (shimDataRequest.getNormalize()) { - SimpleModule module = new SimpleModule(); - module.addDeserializer(ShimDataResponse.class, runkeeperDataType.getNormalizer()); - objectMapper.registerModule(module); - return new ResponseEntity<>(objectMapper.readValue(response.getBody(), - ShimDataResponse.class), HttpStatus.OK); - } else { - return new ResponseEntity<>( - ShimDataResponse.result(RunkeeperShim.SHIM_KEY, objectMapper.readTree(response.getBody())), HttpStatus.OK); + responseEntity = restTemplate.exchange(uriBuilder.build().encode().toUri(), GET, + new HttpEntity(headers), JsonNode.class); + } + catch (HttpClientErrorException | HttpServerErrorException e) { + // FIXME figure out how to handle this + logger.error("A request for RunKeeper data failed.", e); + throw e; + } + + if (shimDataRequest.getNormalize()) { + RunkeeperDataPointMapper dataPointMapper; + switch(runkeeperDataType){ + case ACTIVITY: + dataPointMapper = new RunkeeperPhysicalActivityDataPointMapper(); + break; + case CALORIES: + dataPointMapper = new RunkeeperCaloriesBurnedDataPointMapper(); + break; + default: + throw new UnsupportedOperationException(); } - } catch (IOException e) { - e.printStackTrace(); - throw new ShimException("Could not read response data."); + return ok().body(ShimDataResponse.result(SHIM_KEY, + dataPointMapper.asDataPoints(singletonList(responseEntity.getBody())))); + } + else { + return ok().body(ShimDataResponse.result(SHIM_KEY, responseEntity.getBody())); } } - protected AuthorizationRequestParameters getAuthorizationRequestParameters( - final String username, - final UserRedirectRequiredException exception) { + @Override + protected String getAuthorizationUrl(UserRedirectRequiredException exception) { + final OAuth2ProtectedResourceDetails resource = getResource(); - String authorizationUrl = exception.getRedirectUri() - + "?state=" - + exception.getStateKey() - + "&client_id=" - + resource.getClientId() - + "&response_type=code" - + "&redirect_uri=" + getCallbackUrl(); - AuthorizationRequestParameters parameters = new AuthorizationRequestParameters(); - parameters.setRedirectUri(exception.getRedirectUri()); - parameters.setStateKey(exception.getStateKey()); - parameters.setHttpMethod(HttpMethod.GET); - parameters.setAuthorizationUrl(authorizationUrl); - return parameters; + + UriComponentsBuilder uriBuilder = UriComponentsBuilder + .fromUriString(exception.getRedirectUri()) + .queryParam("state", exception.getStateKey()) + .queryParam("client_id", resource.getClientId()) + .queryParam("response_type", "code") + .queryParam("redirect_uri", getCallbackUrl()); + + return uriBuilder.build().encode().toUriString(); } - /** - * Adds jawbone required parameters to authorization token requests. - */ private class RunkeeperTokenRequestEnhancer implements RequestEnhancer { + @Override public void enhance(AccessTokenRequest request, - OAuth2ProtectedResourceDetails resource, - MultiValueMap form, HttpHeaders headers) { + OAuth2ProtectedResourceDetails resource, + MultiValueMap form, HttpHeaders headers) { + + // TODO code? form.set("client_id", resource.getClientId()); form.set("client_secret", resource.getClientSecret()); form.set("grant_type", resource.getGrantType()); diff --git a/shim-server/src/main/java/org/openmhealth/shim/runkeeper/mapper/RunkeeperBodyWeightDataPointMapper.java b/shim-server/src/main/java/org/openmhealth/shim/runkeeper/mapper/RunkeeperBodyWeightDataPointMapper.java new file mode 100644 index 00000000..23bcadde --- /dev/null +++ b/shim-server/src/main/java/org/openmhealth/shim/runkeeper/mapper/RunkeeperBodyWeightDataPointMapper.java @@ -0,0 +1,40 @@ +/* + * 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.runkeeper.mapper; + +import com.fasterxml.jackson.databind.JsonNode; +import org.openmhealth.schema.domain.omh.BodyWeight; +import org.openmhealth.schema.domain.omh.DataPoint; + +import java.util.Optional; + + +/** + * A mapper from RunKeeper HealthGraph API application/vnd.com.runkeeper.WeightSetFeed+json responses to {@link + * BodyWeight} objects. + * + * @author Emerson Farrugia + * @see API documentation + */ +public class RunkeeperBodyWeightDataPointMapper extends RunkeeperDataPointMapper { + + @Override + protected Optional> asDataPoint(JsonNode itemNode) { + + throw new UnsupportedOperationException("This measure cannot be mapped without time zone information."); + } +} 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 new file mode 100644 index 00000000..887db860 --- /dev/null +++ b/shim-server/src/main/java/org/openmhealth/shim/runkeeper/mapper/RunkeeperCaloriesBurnedDataPointMapper.java @@ -0,0 +1,73 @@ +/* + * 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.runkeeper.mapper; + +import com.fasterxml.jackson.databind.JsonNode; +import org.openmhealth.schema.domain.omh.CaloriesBurned; +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.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. + * + * @author Chris Schaefbauer + * @author Emerson Farrugia + */ +public class RunkeeperCaloriesBurnedDataPointMapper extends RunkeeperDataPointMapper { + + + @Override + protected Optional> asDataPoint(JsonNode itemNode) { + + Optional caloriesBurned = getMeasure(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 + } + + } + + private Optional getMeasure(JsonNode itemNode) { + + Optional calorieValue = asOptionalDouble(itemNode, "total_calories"); + if (!calorieValue.isPresent()) { // Not all activity datapoints have the "total_calories" property + return Optional.empty(); + } + CaloriesBurned.Builder caloriesBurnedBuilder = + new CaloriesBurned.Builder(new KcalUnitValue(KcalUnit.KILOCALORIE, calorieValue.get())); + + setEffectiveTimeFrameIfPresent(itemNode, caloriesBurnedBuilder); + + Optional activityType = asOptionalString(itemNode, "type"); + activityType.ifPresent(at -> caloriesBurnedBuilder.setActivityName(at)); + + 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 new file mode 100644 index 00000000..982910d9 --- /dev/null +++ b/shim-server/src/main/java/org/openmhealth/shim/runkeeper/mapper/RunkeeperDataPointMapper.java @@ -0,0 +1,164 @@ +/* + * 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.runkeeper.mapper; + +import com.fasterxml.jackson.databind.JsonNode; +import org.openmhealth.schema.domain.omh.*; +import org.openmhealth.shim.common.mapper.JsonNodeDataPointMapper; + +import java.time.LocalDateTime; +import java.time.OffsetDateTime; +import java.time.ZoneOffset; +import java.time.format.DateTimeFormatter; +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; + +import static com.google.common.base.Preconditions.checkNotNull; +import static java.util.UUID.randomUUID; +import static org.openmhealth.schema.domain.omh.DurationUnit.SECOND; +import static org.openmhealth.schema.domain.omh.TimeInterval.ofStartDateTimeAndDuration; +import static org.openmhealth.shim.common.mapper.JsonNodeMappingSupport.*; + + +/** + * An abstract mapper for building RunKeeper data points. + * + * @author Emerson Farrugia + */ +public abstract class RunkeeperDataPointMapper implements JsonNodeDataPointMapper { + + public static final String RESOURCE_API_SOURCE_NAME = "Runkeeper HealthGraph API"; + public static final DateTimeFormatter DATE_TIME_FORMATTER = DateTimeFormatter.ofPattern("EEE, d MMM yyyy HH:mm:ss"); + + + @Override + public List> asDataPoints(List responseNodes) { + + // all mapped RunKeeper responses only require a single endpoint response + checkNotNull(responseNodes); + checkNotNull(responseNodes.size() == 1, "A single response node is allowed per call."); + + // all mapped RunKeeper responses contain a single list + JsonNode listNode = asRequiredNode(responseNodes.get(0), getListNodeName()); + + List> dataPoints = new ArrayList<>(); + + for (JsonNode listEntryNode : listNode) { + + // The Runkeeper HealthGraph API does not allow 3rd parties to write the utc_offset property in their + // posts, so we filter out data in the HealthGraph API that does not come directly from Runkeeper. This + // ensures that we can establish a time frame for each activity because we have utc_offset information. + if (!listEntryNode.has("utc_offset")) { + continue; + } + + asDataPoint(listEntryNode).ifPresent(dataPoints::add); + } + + return dataPoints; + } + + /** + * @return the name of the list node used by this mapper + */ + protected String getListNodeName() { + return "items"; + } + + /** + * @return a {@link DataPointHeader} for data points created from Runkeeper HealthGraph API responses + */ + protected DataPointHeader getDataPointHeader(JsonNode itemNode, Measure measure) { + + DataPointAcquisitionProvenance.Builder provenanceBuilder = + new DataPointAcquisitionProvenance.Builder(RESOURCE_API_SOURCE_NAME); + + getModality(itemNode).ifPresent(provenanceBuilder::setModality); + + DataPointAcquisitionProvenance provenance = provenanceBuilder.build(); + + asOptionalString(itemNode, "uri") + .ifPresent(externalId -> provenance.setAdditionalProperty("external_id", externalId)); + + DataPointHeader.Builder headerBuilder = + new DataPointHeader.Builder(randomUUID().toString(), measure.getSchemaId()) + .setAcquisitionProvenance(provenance); + + asOptionalInteger(itemNode, "userId").ifPresent(userId -> headerBuilder.setUserId(userId.toString())); + + return headerBuilder.build(); + + } + + /** + * @see article on modality + */ + // TODO clarify source checks + public Optional getModality(JsonNode itemNode) { + + String source = asOptionalString(itemNode, "source").orElse(null); + String entryMode = asOptionalString(itemNode, "entry_mode").orElse(null); + Boolean hasPath = asOptionalBoolean(itemNode, "has_path").orElse(null); + + if (entryMode != null && entryMode.equals("Web") && source != null && source.equalsIgnoreCase("RunKeeper")) { + + return Optional.of(DataPointModality.SELF_REPORTED); + } + + if (source != null && source.equalsIgnoreCase("RunKeeper") + && entryMode != null && entryMode.equals("API") + && hasPath != null && hasPath) { + + return Optional.of(DataPointModality.SENSED); + } + + 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 + */ + protected void setEffectiveTimeFrameIfPresent(JsonNode itemNode, Measure.Builder builder) { + + Optional localStartDateTime = + asOptionalLocalDateTime(itemNode, "start_time", DATE_TIME_FORMATTER); + + // RunKeeper doesn't support fractional time zones + Optional utcOffset = asOptionalInteger(itemNode, "utc_offset"); + Optional durationInS = asOptionalDouble(itemNode, "duration"); + + if (localStartDateTime.isPresent() && utcOffset.isPresent() && durationInS.isPresent()) { + + OffsetDateTime startDateTime = localStartDateTime.get().atOffset(ZoneOffset.ofHours(utcOffset.get())); + DurationUnitValue duration = new DurationUnitValue(SECOND, durationInS.get()); + + builder.setEffectiveTimeFrame(ofStartDateTimeAndDuration(startDateTime, duration)); + } + } + + /** + * @param listEntryNode the list entry node + * @return the data point mapped to from that entry, unless skipped + */ + protected abstract Optional> asDataPoint(JsonNode listEntryNode); +} 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 new file mode 100644 index 00000000..9ce25e72 --- /dev/null +++ b/shim-server/src/main/java/org/openmhealth/shim/runkeeper/mapper/RunkeeperPhysicalActivityDataPointMapper.java @@ -0,0 +1,64 @@ +/* + * 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.runkeeper.mapper; + +import com.fasterxml.jackson.databind.JsonNode; +import org.openmhealth.schema.domain.omh.DataPoint; +import org.openmhealth.schema.domain.omh.DataPointHeader; +import org.openmhealth.schema.domain.omh.LengthUnitValue; +import org.openmhealth.schema.domain.omh.PhysicalActivity; + +import java.util.Optional; + +import static org.openmhealth.schema.domain.omh.LengthUnit.METER; +import static org.openmhealth.shim.common.mapper.JsonNodeMappingSupport.asOptionalDouble; +import static org.openmhealth.shim.common.mapper.JsonNodeMappingSupport.asRequiredString; + + +/** + * A mapper from RunKeeper HealthGraph API application/vnd.com.runkeeper.FitnessActivityFeed+json responses to {@link + * PhysicalActivity} objects. + * + * @author Emerson Farrugia + * @author Danilo Bonilla + * @see API documentation + */ +public class RunkeeperPhysicalActivityDataPointMapper extends RunkeeperDataPointMapper { + + @Override + protected Optional> asDataPoint(JsonNode itemNode) { + + PhysicalActivity measure = getMeasure(itemNode); + DataPointHeader header = getDataPointHeader(itemNode, measure); + + return Optional.of(new DataPoint<>(header, measure)); + } + + private PhysicalActivity getMeasure(JsonNode itemNode) { + + String activityName = asRequiredString(itemNode, "type"); + + PhysicalActivity.Builder builder = new PhysicalActivity.Builder(activityName); + + setEffectiveTimeFrameIfPresent(itemNode, builder); + + asOptionalDouble(itemNode, "total_distance") + .ifPresent(distanceInM -> builder.setDistance(new LengthUnitValue(METER, distanceInM))); + + return builder.build(); + } +} diff --git a/shim-server/src/main/java/org/openmhealth/shim/withings/Withings.md b/shim-server/src/main/java/org/openmhealth/shim/withings/Withings.md new file mode 100644 index 00000000..95423004 --- /dev/null +++ b/shim-server/src/main/java/org/openmhealth/shim/withings/Withings.md @@ -0,0 +1,120 @@ +### still work in progress + +# api +api url: https://wbsapi.withings.net/ + +# getting started on Withings + +1. register as a developer at http://oauth.withings.com/api +2. Store your consumer key and secret somewhere safe + +## authentication + +### OAuth 1.0a +- protocol: OAuth 1.0 for production environments +- reference: http://oauth.withings.com/api/doc +- authorization URL: https://oauth.withings.com/account/authorize +- request token: https://oauth.withings.com/account/request_token +- access token: https://oauth.withings.com/account/access_token +- scope: Read only, can read all data, provided user allows +- supports refresh tokens: no, user access tokens do not expire +- signature placement: Query signing +- signature method: HMAC-SHA1 +- access token: user access token, oauth_token=&oauth_token_secret= + +# pagination +- supported: Not explicitly specified + +# rate limit + +- 60 requests per minute, 120 requests per minute for GetIntradayActivity +- limit header: none +- remaining header: none +- next reset time header: none + +# data request limit + +- GetActivity and GetSleepSummary - up to 200 days of data per request +- GetIntradyActivity - up to 24 hours of data per request +- GetSleep - up to 7 days of data per request + +# incremental data + +- supported: Most requests support incremental data retrieval based on the “lastmodified” or “updatetime” properties + +# time zone and time representation +Depends on endpoint, most endpoints return time values in unix epoch seconds which are aligned to UTC +- The body measures endpoint uses unix epoch seconds timestamps aligned to UTC for each individual datapoint, however the entire request will return a timezone, which is the user’s profile timezone and this will change as the user changes this and it will be attributed to all of the datapoints returned in the request, regardless of whether those datapoints were generated in that time zone or not. +- Intraday activity uses unix epoch seconds timestamps aligned to UTC as the start and end date for each data point. +- Sleep summary data uses dates that are in the format YYYY-mm-dd to indicate the date, in the user’s time zone, that the sleep session ended. In addition each session has a startdate and enddate property that are aligned to UTC for each individual datapoint +- Activity measures contain a date property in the format YYYY-mm-dd to indicate that date during which all of the activity data has occurred. Withings creates an activity measures datapoint at the end of the day, when the user reaches midnight in whatever time zone they currently occupy. The datapoint is then stamped with that timezone, wherever they end up at the end of the day. + +# endpoints + +## get activity measures +- Endpoints: /v2/measure?action=getactivity +- Reference: http://oauth.withings.com/api/doc#api-Measure-get_activity + +Retrieves a summary of activity information for a user for a given date or range of dates (up to 200 days). The summary includes information about the step count, active calories burned, seconds of soft, moderate, and vigorous activity, + +### Parameters +- Required parameters: userid, oauth authentication information +- Optional parameters: date, startdateymd, enddateymd (all in the form YYYY-mm-dd), without date the response behavior is unknown + +### Response +The response itself contains two properties, “status” and “body”. The “body” property contains the actual content relevant to the request. A request for a single date will be a single object within the “body” property, while a request for multiple days will have an array, “activities”, with a series of JSON objects representing each day. + +There is a time zone returned with each data point in this response and it is the timezone that the user was in when the data point was generated, which is the timezone they are in at midnight at the end of that day. + +## get intraday activity +- Endpoint: /v2/measure?action=getintradayactivity +- Reference: http://oauth.withings.com/api/doc#api-Measure-get_intraday_measure + +Retrieves activity information for active time periods within a single time hour time period. The response contains information includes calories burned, elevation traveled, steps taken, distance traveled, and duration of activity. + +### Parameters +- Required parameters: userid, startdate, enddate (these dates are in the form of unix epoch seconds time stamps, indicating a specific start and end time of interest, within a 24 hour time period) + +### Response +The endpoint returns two properties, “status” and “body.” Body contains one property, “series,” which is a dictionary of activity periods where the key is a unix epoch seconds value for the start time of the activity period and the value is an object describing the activity period by the calories, elevation, steps, distance, and duration. + +Each data point is a defined time period within the day with a unix epoch seconds timestamp value as the start of the activity along with a duration in seconds. Only time periods when activity data exists are returned, so time periods not captured within this response are implicitly defined to be 0 activity periods. It appears that the maximum granularity is at 15 minutes, so data points within the response are separated by 15 minutes or less. A 30 minute activity period would be represented by two 15 minute activity periods and a 25 minute activity period would be represented by a 15 minute activity period followed by a 10 minute activity period. There is no timezone information associated with responses from this endpoint. + +## get body measures +- endpoint: /measure?action=getmeas +- reference: http://oauth.withings.com/api/doc#api-Measure-get_measure + +Retrieves different body measurements for the user, such as weight, height, and blood pressure. + +### Parameters +- required parameters: userid +- optional parameters: meastype, category, limit, offset, startdate, enddate, lastupdated - it is intended to use either lastupdated or startdate/enddate (the behavior for the response to a request without them in unspecified date time frame or lastupdated) + +### Response +The endpoint returns a JSON document with two properties, “status” and “body.” The “body” property contains three properties - “timezone”, “updatetime”, and “measuregrps.” Measuregrps is an array of data points, each capturing a series of measure that were taken at the same time. Each data point in the array has a date field that is the unix epoch seconds value at which the data point was captured. Within each data point, there is an array, “measures,” that contains a list of measures that were taken at that time point. Each measure has a type, which corresponds to the meastype enumeration in Withings (indicating blood pressure vs weight vs height, etc), a value, which is the actual numerical value of the measure (appears to only be integers), and unit, which is not actually a unit value, but instead a decimal shift value indicating the power of ten the "value" parameter should be multiplied to to get the real value. + +## get sleep measures +- endpoint: /v2/sleep?action=get +- reference: http://oauth.withings.com/api/doc#api-Measure-get_sleep + +Retrieves information regarding the time spent in different stages of sleep during sleep periods. + +### Parameters +- required parameters: userid, startdate, enddate (startdate and enddate are timestamps based on unix epoch in seconds) +- optional parameters: none + +### Response +The endpoint returns a JSON document with two properties, “status” and “body.” The “body” property contains a field, “model,” that is an integer that refers to the specific device model used to capture the data point. The body also contains an array, “series,” which contains objects representing different sleep phases during the sleep period specified in the request. Each object has a “startdate” and “enddate” field, which are in unix epoch seconds, and then a “state” field that represents the sleep phase the individual was in during that time (0: awake, 1: light sleep, 2: deep sleep, 3: REM sleep (only if model is Aura)). These data points do not have any time zone data associated with them. + +## get sleep summary +- endpoint: /v2/sleep?action=getsummary +- reference: http://oauth.withings.com/api/doc#api-Measure-get_sleep_summary + +Retrieves the summary of the sleep for each night within the specified date range. This summary includes the seconds of light sleep, deep sleep, rem sleep, the time to sleep, and other aspects of the sleep experience. + +### Parameters +- required parameters: either startdateymd/enddateymd or lastmodified +- optional parameters: none + +### Response +The endpoint returns a JSON document with two properties, “status” and “body.” The “body” property contains a field “more,” that is undescribed and an array “series,” which contains a list of sleep summaries that correspond to individual days within the requested time period. Each item in the list has a date (in YYYY-mm-dd form), startdate and enddate fields (in unix epoch seconds), a timezone, an identifier (no description of the id), the durations of different types of sleep (in seconds), and the counts of sleep variables (such as the number of wakeups). diff --git a/shim-server/src/main/java/org/openmhealth/shim/withings/WithingsConfig.java b/shim-server/src/main/java/org/openmhealth/shim/withings/WithingsConfig.java deleted file mode 100644 index 4285c4ec..00000000 --- a/shim-server/src/main/java/org/openmhealth/shim/withings/WithingsConfig.java +++ /dev/null @@ -1,60 +0,0 @@ -/* - * Copyright 2014 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; - -import org.openmhealth.shim.ApplicationAccessParameters; -import org.openmhealth.shim.ApplicationAccessParametersRepo; -import org.openmhealth.shim.ShimConfig; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.context.properties.ConfigurationProperties; -import org.springframework.stereotype.Component; - -/** - * @author Danilo Bonilla - */ -@Component -@ConfigurationProperties(prefix = "openmhealth.shim.withings") -public class WithingsConfig implements ShimConfig { - - private String clientId; - - private String clientSecret; - - @Autowired - private ApplicationAccessParametersRepo applicationParametersRepo; - - public String getClientId() { - ApplicationAccessParameters parameters = - applicationParametersRepo.findByShimKey(WithingsShim.SHIM_KEY); - return parameters != null ? parameters.getClientId() : clientId; - } - - public void setClientId(String clientId) { - this.clientId = clientId; - } - - public String getClientSecret() { - ApplicationAccessParameters parameters = - applicationParametersRepo.findByShimKey(WithingsShim.SHIM_KEY); - return parameters != null ? parameters.getClientSecret() : clientSecret; - } - - public void setClientSecret(String clientSecret) { - this.clientSecret = clientSecret; - } - -} 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 6f443aba..00d6df05 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 @@ -1,11 +1,11 @@ /* - * Copyright 2014 Open mHealth + * 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 + * 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, @@ -16,42 +16,43 @@ package org.openmhealth.shim.withings; -import com.fasterxml.jackson.core.JsonParser; -import com.fasterxml.jackson.databind.DeserializationContext; -import com.fasterxml.jackson.databind.JsonDeserializer; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; -import com.fasterxml.jackson.databind.module.SimpleModule; -import com.fasterxml.jackson.databind.node.ArrayNode; -import com.jayway.jsonpath.JsonPath; -import net.minidev.json.JSONArray; -import net.minidev.json.JSONObject; import org.apache.http.HttpEntity; import org.apache.http.HttpResponse; import org.apache.http.client.methods.HttpGet; -import org.joda.time.DateTime; -import org.joda.time.DateTimeZone; -import org.joda.time.format.DateTimeFormat; -import org.joda.time.format.DateTimeFormatter; -import org.openmhealth.schema.pojos.*; -import org.openmhealth.schema.pojos.build.*; -import org.openmhealth.schema.pojos.generic.LengthUnitValue; -import org.openmhealth.schema.pojos.generic.MassUnitValue; -import org.openmhealth.schema.pojos.generic.TimeFrame; +import org.openmhealth.schema.domain.omh.DataPoint; import org.openmhealth.shim.*; -import org.springframework.util.CollectionUtils; +import org.openmhealth.shim.withings.domain.WithingsBodyMeasureType; +import org.openmhealth.shim.withings.mapper.*; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.context.properties.ConfigurationProperties; +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; import java.io.IOException; -import java.math.BigDecimal; +import java.io.InputStream; +import java.net.URI; import java.net.URL; -import java.util.*; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; + +import static java.util.Collections.singletonList; -import static org.openmhealth.schema.pojos.generic.DurationUnitValue.DurationUnit.*; /** * @author Danilo Bonilla + * @author Chris Schaefbauer */ +@Component +@ConfigurationProperties(prefix = "openmhealth.shim.withings") public class WithingsShim extends OAuth1ShimBase { public static final String SHIM_KEY = "withings"; @@ -64,13 +65,18 @@ public class WithingsShim extends OAuth1ShimBase { private static final String TOKEN_URL = "https://oauth.withings.com/account/access_token"; - private WithingsConfig config; + private static final String PARTNER_ACCESS_ACTIVITY_ENDPOINT = "getintradayactivity"; + + @Value("${openmhealth.shim.withings.partner_access:false}") + protected boolean partnerAccess; + + @Autowired + public WithingsShim(ApplicationAccessParametersRepo applicationParametersRepo, + AuthorizationRequestParametersRepo authorizationRequestParametersRepo, + ShimServerConfig shimServerConfig) { + + super(applicationParametersRepo, authorizationRequestParametersRepo, shimServerConfig); - public WithingsShim(AuthorizationRequestParametersRepo authorizationRequestParametersRepo, - ShimServerConfig shimServerConfig, - WithingsConfig withingsConfig) { - super(authorizationRequestParametersRepo, shimServerConfig); - this.config = withingsConfig; } @Override @@ -88,16 +94,6 @@ public String getShimKey() { return SHIM_KEY; } - @Override - public String getClientSecret() { - return config.getClientSecret(); - } - - @Override - public String getClientId() { - return config.getClientId(); - } - @Override public String getBaseRequestTokenUrl() { return REQUEST_TOKEN_URL; @@ -115,361 +111,218 @@ public String getBaseTokenUrl() { @Override public ShimDataType[] getShimDataTypes() { - return new ShimDataType[]{ - WithingsDataType.BODY, WithingsDataType.INTRADAY, WithingsDataType.SLEEP + + return new ShimDataType[] { + WithingsDataType.HEART_RATE, WithingsDataType.BLOOD_PRESSURE, WithingsDataType.SLEEP, + WithingsDataType.CALORIES, + WithingsDataType.BODY_HEIGHT, WithingsDataType.STEPS, WithingsDataType.BODY_WEIGHT }; + } @Override protected void loadAdditionalAccessParameters( - HttpServletRequest request, AccessParameters accessParameters) { + + HttpServletRequest request, AccessParameters accessParameters) { Map addlParams = - accessParameters.getAdditionalParameters(); - addlParams = addlParams != null ? addlParams : new LinkedHashMap(); + accessParameters.getAdditionalParameters(); + addlParams = addlParams != null ? addlParams : new LinkedHashMap<>(); + // Withings maintains a unique id, separate from username, for each user and requires that as a parameter + // for requests. Userid is exposed during the authentication process and needed to construct the request URI. addlParams.put("userid", request.getParameter("userid")); - } - - public enum MeasureType { - WEIGHT(1), - HEIGHT(4), - FAT_FREE_MASS(5), - FAT_RATIO(6), - FAT_MASS_WEIGHT(8), - BLOOD_PRESSURE_DIASTOLIC(9), - BLOOD_PRESSURE_SYSTOLIC(10), - HEART_PULSE(11), - SP02(54); - - private int intVal; - - MeasureType(int measureType) { - this.intVal = measureType; - } - - public int getIntVal() { - return intVal; - } - - public static MeasureType valueFor(int intVal) { - for (MeasureType typeEnum : MeasureType.values()) { - if (typeEnum.getIntVal() == intVal) { - return typeEnum; - } - } - return null; - } } public enum WithingsDataType implements ShimDataType { - BODY("measure?action=getmeas&category=1", "startdate", "enddate", true, true, - new JsonDeserializer() { - - @Override - @SuppressWarnings("unchecked") - public ShimDataResponse deserialize(JsonParser jsonParser, - DeserializationContext ctxt) - throws IOException { - - JsonNode responseNode = jsonParser.getCodec().readTree(jsonParser); - String rawJson = responseNode.toString(); - - List bodyWeights = new ArrayList<>(); - List bodyHeights = new ArrayList<>(); - List bloodPressures = new ArrayList<>(); - List heartRates = new ArrayList<>(); - - JsonPath measuresPath = JsonPath.compile("$.body.measuregrps[*]"); - List wMeasureGroups = JsonPath.read(rawJson, measuresPath.getPath()); - if (CollectionUtils.isEmpty(wMeasureGroups)) { - return ShimDataResponse.result(WithingsShim.SHIM_KEY, null); - } - ObjectMapper mapper = new ObjectMapper(); - for (Object wMeasureGroup : wMeasureGroups) { - JsonNode wMeasure = mapper.readTree(((JSONObject) wMeasureGroup).toJSONString()); - DateTime dateTime = new DateTime( - Long.parseLong(wMeasure.get("date").asText()) * 1000l, DateTimeZone.UTC); - ArrayNode measureNodes = (ArrayNode) wMeasure.get("measures"); - SystolicBloodPressure systolic = null; - DiastolicBloodPressure diastolic = null; - for (JsonNode measureNode : measureNodes) { - MeasureType measureType = MeasureType.valueFor(measureNode.get("type").asInt()); - if (measureType == null) { - continue; - } - Double valueAsDouble = measureNode.get("value").asDouble(); - if (valueAsDouble == null) { - continue; - } - Double multiplier = measureNode.get("unit") == null ? - 1 : Math.pow(10, measureNode.get("unit").asDouble()); - BigDecimal measureValue = new BigDecimal(valueAsDouble * multiplier); - switch (measureType) { - case WEIGHT: - bodyWeights.add(new BodyWeightBuilder() - .setTimeTaken(dateTime) - .setWeight(measureValue + "", - MassUnitValue.MassUnit.kg.toString()) - .build()); - break; - case HEIGHT: - bodyHeights.add(new BodyHeightBuilder() - .setTimeTaken(dateTime) - .setHeight( - measureValue + "", - LengthUnitValue.LengthUnit.m.toString()) - .build()); - break; - case BLOOD_PRESSURE_DIASTOLIC: - diastolic = new DiastolicBloodPressure( - measureValue, BloodPressureUnit.mmHg); - break; - case BLOOD_PRESSURE_SYSTOLIC: - systolic = new SystolicBloodPressure( - measureValue, BloodPressureUnit.mmHg); - break; - case HEART_PULSE: - heartRates.add(new HeartRateBuilder() - .withRate(Integer.parseInt(measureValue.intValue() + "")) - .withTimeTaken(dateTime) - .build()); - break; - } - } - if (systolic != null && diastolic != null) { - BloodPressure bloodPressure = new BloodPressure(); - bloodPressure.setSystolic(systolic); - bloodPressure.setDiastolic(diastolic); - bloodPressure.setEffectiveTimeFrame(TimeFrame.withDateTime(dateTime)); - bloodPressures.add(bloodPressure); - } - } - Map results = new HashMap<>(); - results.put(BodyWeight.SCHEMA_BODY_WEIGHT, bodyWeights); - results.put(BodyHeight.SCHEMA_BODY_HEIGHT, bodyHeights); - results.put(BloodPressure.SCHEMA_BLOOD_PRESSURE, bloodPressures); - results.put(HeartRate.SCHEMA_HEART_RATE, heartRates); - return ShimDataResponse.result(WithingsShim.SHIM_KEY, results); - } - }), - - ACTIVITY("v2/measure?action=getactivity", "startdateymd", "enddateymd", false, false, - new JsonDeserializer() { - @Override - public ShimDataResponse deserialize(JsonParser jsonParser, - DeserializationContext ctxt) - throws IOException { - return ShimDataResponse.empty(WithingsShim.SHIM_KEY); - } - }), - - INTRADAY("v2/measure?action=getintradayactivity", "startdate", "enddate", false, true, - new JsonDeserializer() { - - @Override - @SuppressWarnings("unchecked") - public ShimDataResponse deserialize(JsonParser jsonParser, - DeserializationContext ctxt) - throws IOException { - JsonNode responseNode = jsonParser.getCodec().readTree(jsonParser); - String rawJson = responseNode.toString(); - - List steps = new ArrayList<>(); - - JsonPath stepsPath = JsonPath.compile("$.body.series[*]"); - Object wStepsObject = JsonPath.read(rawJson, stepsPath.getPath()); - if (wStepsObject == null || ((JSONArray) wStepsObject).size() == 0) { - return ShimDataResponse.empty(WithingsShim.SHIM_KEY); - } - - ObjectMapper mapper = new ObjectMapper(); - Map wSteps = - mapper.readValue(((JSONObject) wStepsObject).toJSONString(), HashMap.class); - - for (String timestampStr : wSteps.keySet()) { - DateTime dateTime = new DateTime(Long.parseLong(timestampStr) * 1000, DateTimeZone.UTC); - Map stepEntry = (Map) wSteps.get(timestampStr); - - steps.add(new StepCountBuilder() - .withStartAndDuration( - dateTime, Double.parseDouble(stepEntry.get("duration") + ""), sec) - .setSteps((Integer) stepEntry.get("steps")).build()); - } - Map results = new HashMap<>(); - results.put(StepCount.SCHEMA_STEP_COUNT, steps); - return ShimDataResponse.result(WithingsShim.SHIM_KEY, results); - } - }), - - SLEEP("v2/sleep?action=get", "startdate", "enddate", false, true, - new JsonDeserializer() { - @Override - public ShimDataResponse deserialize(JsonParser jsonParser, - DeserializationContext ctxt) - throws IOException { - JsonNode responseNode = jsonParser.getCodec().readTree(jsonParser); - String rawJson = responseNode.toString(); - - List sleeps = new ArrayList<>(); - - JsonPath sleepsPath = JsonPath.compile("$.body.series[*]"); - List wSleeps = JsonPath.read(rawJson, sleepsPath.getPath()); - if (CollectionUtils.isEmpty(wSleeps)) { - return ShimDataResponse.empty(WithingsShim.SHIM_KEY); - } - - ObjectMapper mapper = new ObjectMapper(); - for (Object rawSleep : wSleeps) { - JsonNode wSleep = mapper.readTree(((JSONObject) rawSleep).toJSONString()); - //State = 0 is 'awake', must be filtered out. - if (wSleep.get("state").asInt() != 0) { - DateTime startTime = new DateTime(wSleep.get("startdate").asLong() * 1000, DateTimeZone.UTC); - DateTime endTime = new DateTime(wSleep.get("enddate").asLong() * 1000, DateTimeZone.UTC); - long duration = wSleep.get("enddate").asLong() - wSleep.get("startdate").asLong(); - sleeps.add(new SleepDurationBuilder() - .withStartAndEndAndDuration( - startTime, endTime, - Double.parseDouble(duration + "") / 60d, - SleepDurationUnitValue.Unit.min) - .build()); - } - } - Map results = new HashMap<>(); - results.put(SleepDuration.SCHEMA_SLEEP_DURATION, sleeps); - return ShimDataResponse.result(WithingsShim.SHIM_KEY, results); - } - }); - - private String endPointMethod; - - private String dateParamStart; - - private String dateParamEnd; - - private boolean isNumToReturnSupported; - - private boolean isTimeStampFormat; - - private JsonDeserializer normalizer; - - WithingsDataType(String endPointUrl, - String dateParamStart, - String dateParamEnd, - boolean isNumToReturnSupported, - boolean isTimeStampFormat, - JsonDeserializer normalizer) { - this.endPointMethod = endPointUrl; - this.normalizer = normalizer; - this.dateParamStart = dateParamStart; - this.dateParamEnd = dateParamEnd; - this.isNumToReturnSupported = isNumToReturnSupported; - this.isTimeStampFormat = isTimeStampFormat; - } - - @Override - public JsonDeserializer getNormalizer() { - return normalizer; + BODY_WEIGHT("measure", "getmeas", true), + BODY_HEIGHT("measure", "getmeas", true), + STEPS("v2/measure", "getactivity", false), + CALORIES("v2/measure", "getactivity", false), + SLEEP("v2/sleep", "getsummary", false), + HEART_RATE("measure", "getmeas", true), + BLOOD_PRESSURE("measure", "getmeas", true); + + private String endpoint; + private String measureParameter; + private boolean usesUnixEpochSecondsDate; + + WithingsDataType(String endpoint, String measureParameter, boolean usesUnixEpochSecondsDate) { + this.endpoint = endpoint; + this.measureParameter = measureParameter; + this.usesUnixEpochSecondsDate = usesUnixEpochSecondsDate; } - public String getEndPointMethod() { - return endPointMethod; + public String getEndpoint() { + return endpoint; } - public String getDateParamStart() { - return dateParamStart; + public String getMeasureParameter() { + return measureParameter; } - public String getDateParamEnd() { - return dateParamEnd; - } - - public boolean isNumToReturnSupported() { - return isNumToReturnSupported; - } - - public boolean isTimeStampFormat() { - return isTimeStampFormat; - } } @Override public ShimDataResponse getData(ShimDataRequest shimDataRequest) throws ShimException { + AccessParameters accessParameters = shimDataRequest.getAccessParameters(); String accessToken = accessParameters.getAccessToken(); String tokenSecret = accessParameters.getTokenSecret(); - final String userid = accessParameters.getAdditionalParameters().get("userid").toString(); - String endPointUrl = DATA_URL; + // userid is a unique id associated with each user and returned by Withings in the authorization, this id is + // used as a parameter in the request + final String userid = accessParameters.getAdditionalParameters().get("userid").toString(); final WithingsDataType withingsDataType; try { + withingsDataType = WithingsDataType.valueOf( - shimDataRequest.getDataTypeKey().trim().toUpperCase()); - } catch (NullPointerException | IllegalArgumentException e) { + shimDataRequest.getDataTypeKey().trim().toUpperCase()); + } + catch (NullPointerException | IllegalArgumentException e) { throw new ShimException("Null or Invalid data type parameter: " - + shimDataRequest.getDataTypeKey() - + " in shimDataRequest, cannot retrieve data."); + + shimDataRequest.getDataTypeKey() + + " in shimDataRequest, cannot retrieve data."); } - final DateTimeFormatter formatter = DateTimeFormat.forPattern("yyyy-MM-dd"); + ObjectMapper objectMapper = new ObjectMapper(); + URI uri = createWithingsRequestUri(shimDataRequest, userid, withingsDataType); + URL url = signUrl(uri.toString(), accessToken, tokenSecret, null); - /*** - * Setup default date parameters - */ - DateTime today = new DateTime(); + // TODO: Handle requests for a number of days greater than what Withings supports + HttpGet get = new HttpGet(url.toString()); + HttpResponse response; + try { + response = httpClient.execute(get); + HttpEntity responseEntity = response.getEntity(); - DateTime startDate = shimDataRequest.getStartDate() == null ? - today.minusDays(1) : shimDataRequest.getStartDate(); - String dateStart = startDate.toString(formatter); - long dateStartTs = startDate.toDate().getTime() / 1000; + if (shimDataRequest.getNormalize()) { + WithingsDataPointMapper mapper; + + switch (withingsDataType) { + + case BODY_WEIGHT: + mapper = new WithingsBodyWeightDataPointMapper(); + break; + case BODY_HEIGHT: + mapper = new WithingsBodyHeightDataPointMapper(); + break; + case STEPS: + if (partnerAccess) { + // Use a different mapper because the partner-access endpoint generates a different response + mapper = new WithingsIntradayStepCountDataPointMapper(); + } + else { + mapper = new WithingsDailyStepCountDataPointMapper(); + } + break; + case CALORIES: + if (partnerAccess) { + mapper = new WithingsIntradayCaloriesBurnedDataPointMapper(); + } + else { + mapper = new WithingsDailyCaloriesBurnedDataPointMapper(); + } + break; + case SLEEP: + mapper = new WithingsSleepDurationDataPointMapper(); + break; + case BLOOD_PRESSURE: + mapper = new WithingsBloodPressureDataPointMapper(); + break; + case HEART_RATE: + mapper = new WithingsHeartRateDataPointMapper(); + break; + default: + throw new UnsupportedOperationException(); - DateTime endDate = shimDataRequest.getEndDate() == null ? - today.plusDays(1) : shimDataRequest.getEndDate(); - String dateEnd = endDate.toString(formatter); - long dateEndTs = endDate.toDate().getTime() / 1000; + } - endPointUrl += "/" + withingsDataType.getEndPointMethod(); + InputStream content = responseEntity.getContent(); + JsonNode jsonNode = objectMapper.readValue(content, JsonNode.class); + List dataPoints = mapper.asDataPoints(singletonList(jsonNode)); + return ShimDataResponse.result(WithingsShim.SHIM_KEY, + dataPoints); + } + else { + return ShimDataResponse + .result(WithingsShim.SHIM_KEY, objectMapper.readTree(responseEntity.getContent())); + } - //"&meastype=4"; + } + catch (IOException e) { + throw new ShimException("Could not fetch data", e); + } + finally { + get.releaseConnection(); + } + } + + URI createWithingsRequestUri(ShimDataRequest shimDataRequest, String userid, + WithingsDataType withingsDataType) { - endPointUrl += "&userid=" + userid; + MultiValueMap dateTimeMap = new LinkedMultiValueMap<>(); + if (withingsDataType.usesUnixEpochSecondsDate || isPartnerAccessActivityMeasure(withingsDataType)) { + //the partner access endpoints for activity also use epoch secs - if (withingsDataType.isTimeStampFormat()) { - endPointUrl += "&" + withingsDataType.getDateParamStart() + "=" + dateStartTs; - endPointUrl += "&" + withingsDataType.getDateParamEnd() + "=" + dateEndTs; - } else { - endPointUrl += "&" + withingsDataType.getDateParamStart() + "=" + dateStart; - endPointUrl += "&" + withingsDataType.getDateParamEnd() + "=" + dateEnd; + dateTimeMap.add("startdate", String.valueOf(shimDataRequest.getStartDateTime().toEpochSecond())); + dateTimeMap.add("enddate", String.valueOf(shimDataRequest.getEndDateTime().plusDays(1).toEpochSecond())); } + else { + dateTimeMap.add("startdateymd", shimDataRequest.getStartDateTime().toLocalDate().toString()); + dateTimeMap.add("enddateymd", shimDataRequest.getEndDateTime().toLocalDate().toString()); - if (withingsDataType.isNumToReturnSupported() - && shimDataRequest.getNumToReturn() != null) { - endPointUrl += "&limit=" + shimDataRequest.getNumToReturn(); } - URL url = signUrl(endPointUrl, accessToken, tokenSecret, null); + UriComponentsBuilder uriComponentsBuilder = UriComponentsBuilder.fromUriString(DATA_URL).pathSegment( + withingsDataType.getEndpoint()); + String measureParameter; + if (isPartnerAccessActivityMeasure(withingsDataType)) { + // partner level access allows greater detail around activity, but uses a different endpoint + measureParameter = PARTNER_ACCESS_ACTIVITY_ENDPOINT; + } + else { + measureParameter = withingsDataType.getMeasureParameter(); + } + uriComponentsBuilder.queryParam("action", measureParameter).queryParam("userid", userid) + .queryParams(dateTimeMap); + + // if it's a body measure + if (Objects.equals(withingsDataType.getMeasureParameter(), "getmeas")) { + + /* + The Withings API allows us to query for single body measures, which we take advantage of to reduce + unnecessary data transfer. However, since blood pressure is represented as two separate measures, + namely a diastolic and a systolic measure, when the measure type is blood pressure we ask for all + measures and then filter out the ones we don't care about. + */ + if (withingsDataType != WithingsDataType.BLOOD_PRESSURE) { + + WithingsBodyMeasureType measureType = WithingsBodyMeasureType.valueOf(withingsDataType.name()); + uriComponentsBuilder.queryParam("meastype", measureType.getMagicNumber()); + } - // Fetch and decode the JSON data. - ObjectMapper objectMapper = new ObjectMapper(); - HttpGet get = new HttpGet(url.toString()); - HttpResponse response; - try { - response = httpClient.execute(get); - HttpEntity responseEntity = response.getEntity(); + uriComponentsBuilder.queryParam("category", 1); //filter out goal datapoints - if (shimDataRequest.getNormalize()) { - SimpleModule module = new SimpleModule(); - module.addDeserializer(ShimDataResponse.class, withingsDataType.getNormalizer()); - objectMapper.registerModule(module); - return objectMapper.readValue(responseEntity.getContent(), ShimDataResponse.class); - } else { - return ShimDataResponse.result(WithingsShim.SHIM_KEY, objectMapper.readTree(responseEntity.getContent())); - } - } catch (IOException e) { - throw new ShimException("Could not fetch data", e); - } finally { - get.releaseConnection(); } + + UriComponents uriComponents = uriComponentsBuilder.build(); + return uriComponents.toUri(); + + } + + /** + * Determines whether the request is a Withings partner-access level activity 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 + */ + private boolean isPartnerAccessActivityMeasure(WithingsDataType withingsDataType) { + + return (partnerAccess && + (withingsDataType == WithingsDataType.STEPS || withingsDataType == WithingsDataType.CALORIES)); + } } 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 new file mode 100644 index 00000000..4379651b --- /dev/null +++ b/shim-server/src/main/java/org/openmhealth/shim/withings/domain/WithingsBodyMeasureType.java @@ -0,0 +1,53 @@ +/* + * 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.withings.domain; + + +/** + * A body measure type included in responses from the Withings body measure endpoint, specifically in a 'meastype' + * property. + * + * @author Chris Schaefbauer + * @author Emerson Farrugia + * @see {@link org.openmhealth.shim.withings.mapper.WithingsBodyMeasureDataPointMapper} + * @see Body Measures API documentation + */ +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); + + private int magicNumber; + + WithingsBodyMeasureType(int magicNumber) { + this.magicNumber = magicNumber; + } + + /** + * @return the magic number used to refer to this body measure type in responses + */ + public int getMagicNumber() { + return magicNumber; + } +} diff --git a/shim-server/src/main/java/org/openmhealth/shim/withings/domain/WithingsMeasureGroupAttribution.java b/shim-server/src/main/java/org/openmhealth/shim/withings/domain/WithingsMeasureGroupAttribution.java new file mode 100644 index 00000000..b6679f3a --- /dev/null +++ b/shim-server/src/main/java/org/openmhealth/shim/withings/domain/WithingsMeasureGroupAttribution.java @@ -0,0 +1,87 @@ +/* + * Copyright 2015 Open mHealth + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.openmhealth.shim.withings.domain; + +import java.util.Optional; + + +/** + * A measure group attribution included in responses from the Withings body measure endpoint, specifically in an + * 'attrib' property. The attribution defines whether the user that created the measure group is ambiguous, and whether + * the measure group was sensed or self-reported. + * + * @author Chris Schaefbauer + * @author Emerson Farrugia + * @see {@link org.openmhealth.shim.withings.mapper.WithingsBodyMeasureDataPointMapper} + * @see Body Measures API documentation + */ +public enum WithingsMeasureGroupAttribution { + + SENSED_AND_UNAMBIGUOUS(0, true, false), + SENSED_BUT_AMBIGUOUS(1, true, true), + SELF_REPORTED(2, false, false), + SELF_REPORTED_DURING_CREATION(4, false, false); // TODO confirm + + private int magicNumber; + private boolean sensed; + private boolean ambiguous; + + WithingsMeasureGroupAttribution(int magicNumber, boolean sensed, boolean ambiguous) { + + this.magicNumber = magicNumber; + this.sensed = sensed; + this.ambiguous = ambiguous; + } + + /** + * @return the magic number used to refer to this attribution in responses + */ + public int getMagicNumber() { + return magicNumber; + } + + /** + * @return true if the measure was sensed, false if it was self-reported + */ + public boolean isSensed() { + return sensed; + } + + /** + * @return true if the measure may belong to more than one user, false if it is known to belong to one user. + * According to Withings, this can happen for weight measure groups when a measurement is taken before a new + * user is synced to the device. + */ + public boolean isAmbiguous() { + return ambiguous; + } + + /** + * @param magicNumber a magic number + * @return the constant corresponding to the magic number + */ + public static Optional findByMagicNumber(Integer magicNumber) { + + for (WithingsMeasureGroupAttribution constant : values()) { + if (constant.getMagicNumber() == magicNumber) { + return Optional.of(constant); + } + } + + return Optional.empty(); + } +} diff --git a/shim-server/src/main/java/org/openmhealth/shim/withings/mapper/WithingsBloodPressureDataPointMapper.java b/shim-server/src/main/java/org/openmhealth/shim/withings/mapper/WithingsBloodPressureDataPointMapper.java new file mode 100644 index 00000000..58918fe8 --- /dev/null +++ b/shim-server/src/main/java/org/openmhealth/shim/withings/mapper/WithingsBloodPressureDataPointMapper.java @@ -0,0 +1,66 @@ +/* + * 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.withings.mapper; + +import com.fasterxml.jackson.databind.JsonNode; +import org.openmhealth.schema.domain.omh.BloodPressure; +import org.openmhealth.schema.domain.omh.DiastolicBloodPressure; +import org.openmhealth.schema.domain.omh.Measure; +import org.openmhealth.schema.domain.omh.SystolicBloodPressure; +import org.slf4j.Logger; + +import java.math.BigDecimal; +import java.util.Optional; + +import static java.util.Optional.empty; +import static org.openmhealth.schema.domain.omh.BloodPressureUnit.MM_OF_MERCURY; +import static org.openmhealth.shim.withings.domain.WithingsBodyMeasureType.DIASTOLIC_BLOOD_PRESSURE; +import static org.openmhealth.shim.withings.domain.WithingsBodyMeasureType.SYSTOLIC_BLOOD_PRESSURE; +import static org.slf4j.LoggerFactory.getLogger; + + +/** + * @author Chris Schaefbauer + * @author Emerson Farrugia + * @see Body Measures API documentation + */ +public class WithingsBloodPressureDataPointMapper extends WithingsBodyMeasureDataPointMapper { + + private static final Logger logger = getLogger(WithingsBloodPressureDataPointMapper.class); + + @Override + public Optional> newMeasureBuilder(JsonNode measuresNode) { + + Optional systolicValue = getValueForMeasureType(measuresNode, SYSTOLIC_BLOOD_PRESSURE); + Optional diastolicValue = getValueForMeasureType(measuresNode, DIASTOLIC_BLOOD_PRESSURE); + + if (!systolicValue.isPresent() && !diastolicValue.isPresent()) { + return empty(); + } + + if (!systolicValue.isPresent() || !diastolicValue.isPresent()) { + logger.warn("The Withings measure node {} doesn't contain both systolic and diastolic measures.", + measuresNode); + return empty(); + } + + return Optional.of(new BloodPressure.Builder( + new SystolicBloodPressure(MM_OF_MERCURY, systolicValue.get()), + new DiastolicBloodPressure(MM_OF_MERCURY, diastolicValue.get()) + )); + } +} 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 new file mode 100644 index 00000000..e2b62ed8 --- /dev/null +++ b/shim-server/src/main/java/org/openmhealth/shim/withings/mapper/WithingsBodyHeightDataPointMapper.java @@ -0,0 +1,50 @@ +/* + * 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.withings.mapper; + +import com.fasterxml.jackson.databind.JsonNode; +import org.openmhealth.schema.domain.omh.BodyHeight; +import org.openmhealth.schema.domain.omh.LengthUnitValue; +import org.openmhealth.schema.domain.omh.Measure; + +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; + + +/** + * @author Chris Schaefbauer + * @author Emerson Farrugia + * @see Body Measures API documentation + */ +public class WithingsBodyHeightDataPointMapper extends WithingsBodyMeasureDataPointMapper { + + @Override + public Optional> newMeasureBuilder(JsonNode measuresNode) { + + Optional value = getValueForMeasureType(measuresNode, BODY_HEIGHT); + + if (!value.isPresent()) { + return empty(); + } + + return Optional.of(new BodyHeight.Builder(new LengthUnitValue(METER, value.get()))); + } +} 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 new file mode 100644 index 00000000..854207a3 --- /dev/null +++ b/shim-server/src/main/java/org/openmhealth/shim/withings/mapper/WithingsBodyMeasureDataPointMapper.java @@ -0,0 +1,216 @@ +/* + * 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.withings.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.Measure; +import org.openmhealth.shim.common.mapper.JsonNodeMappingException; +import org.openmhealth.shim.withings.domain.WithingsBodyMeasureType; +import org.openmhealth.shim.withings.domain.WithingsMeasureGroupAttribution; +import org.slf4j.Logger; + +import java.math.BigDecimal; +import java.time.Instant; +import java.time.OffsetDateTime; +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.checkNotNull; +import static java.lang.String.format; +import static java.time.ZoneOffset.UTC; +import static org.openmhealth.shim.common.mapper.JsonNodeMappingSupport.*; +import static org.slf4j.LoggerFactory.getLogger; + + +/** + * An abstract mapper from Withings body measure endpoint responses (/measure?action=getmeas) to data points containing + * corresponding measure objects. + * + * @param the measure to map to + * @author Chris Schaefbauer + * @author Emerson Farrugia + * @see Body Measures API documentation + */ +public abstract class WithingsBodyMeasureDataPointMapper extends WithingsDataPointMapper { + + private static final Logger logger = getLogger(WithingsBodyMeasureDataPointMapper.class); + + @Override + public List> asDataPoints(List responseNodes) { + + checkNotNull(responseNodes); + checkNotNull(responseNodes.size() == 1, "A single response node is allowed per call."); + + JsonNode responseNodeBody = asRequiredNode(responseNodes.get(0), BODY_NODE_PROPERTY); + List> dataPoints = Lists.newArrayList(); + + for (JsonNode measureGroupNode : asRequiredNode(responseNodeBody, "measuregrps")) { + + if (isGoal(measureGroupNode)) { + continue; + } + + if (isOwnerAmbiguous(measureGroupNode)) { + logger.warn("The following Withings measure group is being ignored because its owner is ambiguous.\n{}", + measureGroupNode); + continue; + } + + JsonNode measuresNode = asRequiredNode(measureGroupNode, "measures"); + + Measure.Builder measureBuilder = newMeasureBuilder(measuresNode).orElse(null); + if (measureBuilder == null) { + continue; + } + + Optional dateTimeInEpochSeconds = asOptionalLong(measureGroupNode, "date"); + if (dateTimeInEpochSeconds.isPresent()) { + + Instant dateTimeInstant = Instant.ofEpochSecond(dateTimeInEpochSeconds.get()); + measureBuilder.setEffectiveTimeFrame(OffsetDateTime.ofInstant(dateTimeInstant, UTC)); + } + + Optional userComment = asOptionalString(measureGroupNode, "comment"); + if (userComment.isPresent()) { + measureBuilder.setUserNotes(userComment.get()); + } + + T measure = measureBuilder.build(); + + Optional externalId = asOptionalLong(measureGroupNode, "grpid"); + + dataPoints.add(newDataPoint(measure, externalId.orElse(null), isSensed(measureGroupNode), null)); + } + + return dataPoints; + } + + /** + * @return true if the measure group is a goal, or false otherwise + */ + protected boolean isGoal(JsonNode measureGroupNode) { + + int categoryValue = asRequiredInteger(measureGroupNode, "category"); + + if (categoryValue == 1) { + return false; + } + else if (categoryValue == 2) { + return true; + } + + throw new JsonNodeMappingException(format( + "The following Withings measure group node has an unrecognized category value.\n%s", measureGroupNode)); + } + + /** + * @return true if the measure group can't be attributed to a single user, or false if it can + * @see {@link WithingsMeasureGroupAttribution#isAmbiguous()} + */ + protected boolean isOwnerAmbiguous(JsonNode measureGroupNode) { + + return getMeasureGroupAttribution(measureGroupNode) + .orElseThrow(() -> new JsonNodeMappingException(format( + "The following Withings measure group node doesn't contain an attribution property.\n%s", + measureGroupNode))) + .isAmbiguous(); + } + + /** + * @see {@link WithingsMeasureGroupAttribution} + */ + protected Optional getMeasureGroupAttribution(JsonNode measureGroupNode) { + + Optional attributionValue = asOptionalInteger(measureGroupNode, "attrib"); + + if (attributionValue.isPresent()) { + Optional attribution = + WithingsMeasureGroupAttribution.findByMagicNumber(attributionValue.get().intValue()); + + if (attribution.isPresent()) { + return attribution; + } + + throw new JsonNodeMappingException(format( + "The following Withings measure group node has an unrecognized attribution value.\n%s", + measureGroupNode)); + } + else { + return Optional.empty(); + } + } + + /** + * @return true if the measure group was sensed, or false if it was self-reported + */ + protected boolean isSensed(JsonNode measureGroupNode) { + + return getMeasureGroupAttribution(measureGroupNode) + .orElseThrow(() -> new JsonNodeMappingException(format( + "The following Withings measure group node doesn't contain an attribution property.\n%s", + measureGroupNode))) + .isSensed(); + } + + /** + * @param measuresNode the list of measures in a measure group node + * @return a measure builder initialised with the data in the measures list + */ + abstract Optional> newMeasureBuilder(JsonNode measuresNode); + + /** + * @return a {@link BigDecimal} corresponding to the specified measure node + */ + protected BigDecimal getValue(JsonNode measureNode) { + + long unscaledValue = asRequiredLong(measureNode, "value"); + int scale = asRequiredInteger(measureNode, "unit"); + + return BigDecimal.valueOf(unscaledValue, -1 * scale); + + } + + /** + * @param measuresNode the list of measures in a measure group node + * @param bodyMeasureType the measure type of interest + * @return the value of the specified measure type, if present + */ + protected Optional getValueForMeasureType(JsonNode measuresNode, + WithingsBodyMeasureType bodyMeasureType) { + + List values = StreamSupport.stream(measuresNode.spliterator(), false) + .filter((measureNode) -> asRequiredLong(measureNode, "type") == bodyMeasureType.getMagicNumber()) + .map(this::getValue) + .collect(Collectors.toList()); + + if (values.isEmpty()) { + return Optional.empty(); + } + + if (values.size() > 1) { + throw new JsonNodeMappingException(format( + "The following Withings measures node contains multiple measures of type %s.\n%s.", + bodyMeasureType, measuresNode)); + } + + return Optional.of(values.get(0)); + } +} 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 new file mode 100644 index 00000000..1ac6123b --- /dev/null +++ b/shim-server/src/main/java/org/openmhealth/shim/withings/mapper/WithingsBodyWeightDataPointMapper.java @@ -0,0 +1,50 @@ +/* + * 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.withings.mapper; + +import com.fasterxml.jackson.databind.JsonNode; +import org.openmhealth.schema.domain.omh.BodyWeight; +import org.openmhealth.schema.domain.omh.MassUnitValue; +import org.openmhealth.schema.domain.omh.Measure; + +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; + + +/** + * @author Chris Schaefbauer + * @author Emerson Farrugia + * @see Body Measures API documentation + */ +public class WithingsBodyWeightDataPointMapper extends WithingsBodyMeasureDataPointMapper { + + @Override + public Optional> newMeasureBuilder(JsonNode measuresNode) { + + Optional value = getValueForMeasureType(measuresNode, BODY_WEIGHT); + + if (!value.isPresent()) { + return empty(); + } + + return Optional.of(new BodyWeight.Builder(new MassUnitValue(KILOGRAM, value.get()))); + } +} 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 new file mode 100644 index 00000000..313f6a5e --- /dev/null +++ b/shim-server/src/main/java/org/openmhealth/shim/withings/mapper/WithingsDailyCaloriesBurnedDataPointMapper.java @@ -0,0 +1,101 @@ +/* + * 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.withings.mapper; + +import com.fasterxml.jackson.databind.JsonNode; +import org.openmhealth.schema.domain.omh.*; + +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; + + +/** + * 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.

+ * + * @author Chris Schaefbauer + * @see Activity Measures API documentation + */ +public class WithingsDailyCaloriesBurnedDataPointMapper extends WithingsListDataPointMapper { + + /** + * 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.

+ * + * @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) { + + 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)); + } + + Optional userComment = asOptionalString(node, "comment"); + if (userComment.isPresent()) { + caloriesBurnedBuilder.setUserNotes(userComment.get()); + } + + CaloriesBurned caloriesBurned = caloriesBurnedBuilder.build(); + DataPoint caloriesBurnedDataPoint = + newDataPoint(caloriesBurned, null, true, null); + + return Optional.of(caloriesBurnedDataPoint); + + } + + @Override + String getListNodeName() { + return "activities"; + } +} 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 new file mode 100644 index 00000000..80ecf5dc --- /dev/null +++ b/shim-server/src/main/java/org/openmhealth/shim/withings/mapper/WithingsDailyStepCountDataPointMapper.java @@ -0,0 +1,90 @@ +/* + * 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.withings.mapper; + +import com.fasterxml.jackson.databind.JsonNode; +import org.openmhealth.schema.domain.omh.DataPoint; +import org.openmhealth.schema.domain.omh.StepCount; +import org.openmhealth.schema.domain.omh.TimeInterval; + +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; + + +/** + * A mapper from Withings Activity Measures endpoint responses (/measure?action=getactivity) to {@link StepCount} + * objects. + *

+ *

Note: the start datetime and end datetime values for the mapped {@link StepCount} {@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 + * @see Activity Measures API documentation + */ +public class WithingsDailyStepCountDataPointMapper extends WithingsListDataPointMapper { + + /** + * Maps an individual list node from the array in the Withings activity measure endpoint response into a {@link + * StepCount} 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 StepCount} measure with the appropriate values from + * the JSON node parameter, wrapped as an {@link Optional} + */ + @Override + Optional> asDataPoint(JsonNode node) { + + long stepValue = asRequiredLong(node, "steps"); + StepCount.Builder stepCountBuilder = new StepCount.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()); + } + + StepCount stepCount = stepCountBuilder.build(); + DataPoint stepCountDataPoint = newDataPoint(stepCount, null, true, null); + + return Optional.of(stepCountDataPoint); + } + + @Override + String getListNodeName() { + return "activities"; + } +} 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 new file mode 100644 index 00000000..ce6752c3 --- /dev/null +++ b/shim-server/src/main/java/org/openmhealth/shim/withings/mapper/WithingsDataPointMapper.java @@ -0,0 +1,75 @@ +/* + * 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.withings.mapper; + +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 static java.util.UUID.randomUUID; +import static org.openmhealth.schema.domain.omh.DataPointModality.SELF_REPORTED; +import static org.openmhealth.schema.domain.omh.DataPointModality.SENSED; + + +/** + * @author Chris Schaefbauer + */ +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"; + + /** + * A convenience method that creates a {@link DataPoint} from a measure and its metadata. + * + * @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 + * @return the constructed data point + */ + protected DataPoint newDataPoint(T measure, Long externalId, Boolean sensed, + String deviceName) { + + DataPointAcquisitionProvenance.Builder provenanceBuilder = + new DataPointAcquisitionProvenance.Builder(RESOURCE_API_SOURCE_NAME); + + if (sensed != null) { + provenanceBuilder.setModality(sensed ? SENSED : SELF_REPORTED); + } + + // additional properties are always subject to change + DataPointAcquisitionProvenance acquisitionProvenance = provenanceBuilder.build(); + + if (deviceName != null) { + acquisitionProvenance.setAdditionalProperty("device_name", deviceName); + } + + if (externalId != null) { + // TODO discuss the name of the external identifier, to make it clear it's the ID used by the source + acquisitionProvenance.setAdditionalProperty("external_id", externalId); + } + + DataPointHeader header = new DataPointHeader.Builder(randomUUID().toString(), measure.getSchemaId()) + .setAcquisitionProvenance(acquisitionProvenance) + .build(); + + return new DataPoint<>(header, measure); + } +} 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 new file mode 100644 index 00000000..d08ede5f --- /dev/null +++ b/shim-server/src/main/java/org/openmhealth/shim/withings/mapper/WithingsHeartRateDataPointMapper.java @@ -0,0 +1,48 @@ +/* + * Copyright 2015 Open mHealth + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.openmhealth.shim.withings.mapper; + +import com.fasterxml.jackson.databind.JsonNode; +import org.openmhealth.schema.domain.omh.HeartRate; +import org.openmhealth.schema.domain.omh.Measure; + +import java.math.BigDecimal; +import java.util.Optional; + +import static java.util.Optional.empty; +import static org.openmhealth.shim.withings.domain.WithingsBodyMeasureType.HEART_RATE; + + +/** + * @author Chris Schaefbauer + * @author Emerson Farrugia + * @see Body Measures API documentation + */ +public class WithingsHeartRateDataPointMapper extends WithingsBodyMeasureDataPointMapper { + + @Override + public Optional> newMeasureBuilder(JsonNode measuresNode) { + + Optional value = getValueForMeasureType(measuresNode, HEART_RATE); + + if (!value.isPresent()) { + return empty(); + } + + return Optional.of(new HeartRate.Builder(value.get())); + } +} 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 new file mode 100644 index 00000000..03d53ae5 --- /dev/null +++ b/shim-server/src/main/java/org/openmhealth/shim/withings/mapper/WithingsIntradayCaloriesBurnedDataPointMapper.java @@ -0,0 +1,156 @@ +/* + * 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.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.*; + + +/** + * A mapper from Withings Intraday Activity endpoint responses (/measure?action=getactivity) to {@link CaloriesBurned} + * 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 Chris Schaefbauer + * @see Intrday Activity Measures API + * documentation + */ +public class WithingsIntradayCaloriesBurnedDataPointMapper extends WithingsDataPointMapper { + + /** + * 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)); + + } + + /** + * 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()); + } + + } + + +} 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 new file mode 100644 index 00000000..c1098373 --- /dev/null +++ b/shim-server/src/main/java/org/openmhealth/shim/withings/mapper/WithingsIntradayStepCountDataPointMapper.java @@ -0,0 +1,143 @@ +/* + * 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.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.*; + + +/** + * A mapper from Withings Intraday Activity endpoint responses (/measure?action=getactivity) to {@link StepCount} + * 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 Chris Schaefbauer + * @see Activity Measures API documentation + */ +public class WithingsIntradayStepCountDataPointMapper extends WithingsDataPointMapper { + + /** + * Maps JSON response nodes from the intraday activities endpoint (measure?action=getintradayactivity) in the + * Withings API into a list of {@link StepCount} {@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 StepCount} 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; + + } + + /** + * Maps an individual list node from the array in the Withings activity measure endpoint response into a {@link + * StepCount} 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 StepCount} 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"); + StepCount.Builder stepCountBuilder = new StepCount.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()); + } + + StepCount 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; + } + + /** + * 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()); + } + } + + +} 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 new file mode 100644 index 00000000..f4afcaa2 --- /dev/null +++ b/shim-server/src/main/java/org/openmhealth/shim/withings/mapper/WithingsListDataPointMapper.java @@ -0,0 +1,83 @@ +/* + * 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.withings.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.Measure; + +import java.util.List; +import java.util.Optional; + +import static com.google.common.base.Preconditions.checkNotNull; +import static org.openmhealth.shim.common.mapper.JsonNodeMappingSupport.asRequiredNode; + + +/** + * The base class for mappers that translate Withings API responses with datapoints contained in an array to {@link + * Measure} objects. + * + * @author Chris Schaefbauer + * @author Emerson Farrugia + */ +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} + * objects + * with the appropriate measure. Splits individual nodes based on the name of the list node and then iteratively + * maps the nodes in the list. + * + * @param responseNodes a list of a single JSON node containing the entire response from a Withings endpoint + * @return a list of DataPoint objects of type T with the appropriate values mapped from the input JSON; because + * these JSON objects are contained within an array in the input response, each object in that array will map into + * an item in the list + */ + @Override + public List> asDataPoints(List responseNodes) { + + checkNotNull(responseNodes); + checkNotNull(responseNodes.size() == 1, "A single response node is allowed per call."); + + JsonNode listNode = asRequiredNode(responseNodes.get(0), BODY_NODE_PROPERTY + "." + getListNodeName()); + + List> dataPoints = Lists.newArrayList(); + + for (JsonNode listEntryNode : listNode) { + asDataPoint(listEntryNode).ifPresent(dataPoints::add); + } + + return dataPoints; + } + + // TODO fix this + /** + * Abstract method to be implemented by subclasses mapping a JSON response node from the Withings API into a {@link + * Measure} object of the appropriate type. + * + * @param node JSON response from the Withings API endpoint + * @return a {@link DataPoint} object containing the target measure with the appropriate values from the JSON node + * parameter, wrapped as an {@link Optional} + */ + abstract Optional> asDataPoint(JsonNode node); + + /** + * @return the name of the list node used by this mapper + */ + abstract String getListNodeName(); +} 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 new file mode 100644 index 00000000..cc7d598f --- /dev/null +++ b/shim-server/src/main/java/org/openmhealth/shim/withings/mapper/WithingsSleepDurationDataPointMapper.java @@ -0,0 +1,144 @@ +/* + * 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.withings.mapper; + +import com.fasterxml.jackson.databind.JsonNode; +import org.openmhealth.schema.domain.omh.*; + +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 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 SleepDuration} + * objects. + * + * @author Chris Schaefbauer + * @see Sleep Summary API documentation + */ +public class WithingsSleepDurationDataPointMapper extends WithingsListDataPointMapper { + + /** + * Maps an individual list node from the array in the Withings sleep summary endpoint response into a {@link + * SleepDuration} 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 SleepDuration} measure with the appropriate values from + * the JSON node parameter, wrapped as an {@link Optional} + */ + @Override + Optional> asDataPoint(JsonNode node) { + + Long lightSleepInSeconds = asRequiredLong(node, "data.lightsleepduration"); + Long deepSleepInSeconds = asRequiredLong(node, "data.deepsleepduration"); + Long remSleepInSeconds = asRequiredLong(node, "data.remsleepduration"); + + Long totalSleepInSeconds = lightSleepInSeconds + deepSleepInSeconds + remSleepInSeconds; + + SleepDuration.Builder sleepDurationBuilder = + new SleepDuration.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)); + } + + Optional externalId = asOptionalLong(node, "id"); + Optional modelId = asOptionalLong(node, "model"); + String modelName = null; + + if (modelId.isPresent()) { + modelName = SleepDeviceTypes.valueOf(modelId.get()); + } + + SleepDuration sleepDuration = sleepDurationBuilder.build(); + Optional wakeupCount = asOptionalLong(node, "data.wakeupcount"); + if (wakeupCount.isPresent()) { + sleepDuration.setAdditionalProperty("wakeup_count", new Integer(wakeupCount.get().intValue())); + } + + // 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())); + } + + return Optional.of(newDataPoint(sleepDuration, externalId.orElse(null), true, modelName)); + } + + @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); + } + } +} diff --git a/shim-server/src/main/resources/application.yaml b/shim-server/src/main/resources/application.yaml index 638f0f51..a613f73d 100644 --- a/shim-server/src/main/resources/application.yaml +++ b/shim-server/src/main/resources/application.yaml @@ -10,7 +10,7 @@ spring: uri: mongodb://mongo:27017/omh_dsu jackson: serialization: - INDENT_OUTPUT: true + indent_output: true server: port: 8083 security: @@ -31,14 +31,24 @@ openmhealth: #fatsecret: # clientId: [YOUR_CLIENT_ID] # clientSecret: [YOUR_CLIENT_SECRET] + #ihealth: + # clientId: [YOUR_CLIENT_ID] + # clientSecret: [YOUR_CLIENT_SECRET] #jawbone: # clientId: [YOUR_CLIENT_ID] # clientSecret: [YOUR_CLIENT_SECRET] + #misfit: + # clientId: [YOUR_CLIENT_ID] + # clientSecret: [YOUR_CLIENT_SECRET] #runkeeper: # clientId: [YOUR_CLIENT_ID] # clientSecret: [YOUR_CLIENT_SECRET] #withings: + # partner_access: true # clientId: [YOUR_CLIENT_ID] # clientSecret: [YOUR_CLIENT_SECRET] #healthvault: # clientId: [YOUR_CLIENT_ID] + #googlefit: + # clientId: [YOUR_CLIENT_ID] + # clientSecret: [YOUR_CLIENT_SECRET] diff --git a/shim-server/src/test/java/org/openmhealth/shim/common/mapper/DataPointMapperUnitTests.java b/shim-server/src/test/java/org/openmhealth/shim/common/mapper/DataPointMapperUnitTests.java new file mode 100644 index 00000000..83c78005 --- /dev/null +++ b/shim-server/src/test/java/org/openmhealth/shim/common/mapper/DataPointMapperUnitTests.java @@ -0,0 +1,14 @@ +package org.openmhealth.shim.common.mapper; + +import com.fasterxml.jackson.databind.ObjectMapper; + +import static org.openmhealth.schema.configuration.JacksonConfiguration.newObjectMapper; + + +/** + * @author Emerson Farrugia + */ +public abstract class DataPointMapperUnitTests { + + protected static final ObjectMapper objectMapper = newObjectMapper(); +} diff --git a/shim-server/src/test/java/org/openmhealth/shim/fitbit/FitbitShimTest.java b/shim-server/src/test/java/org/openmhealth/shim/fitbit/FitbitShimTest.java deleted file mode 100644 index 83b3f7cf..00000000 --- a/shim-server/src/test/java/org/openmhealth/shim/fitbit/FitbitShimTest.java +++ /dev/null @@ -1,70 +0,0 @@ -/* - * Copyright 2014 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; - -import com.fasterxml.jackson.databind.ObjectMapper; -import com.fasterxml.jackson.databind.module.SimpleModule; -import org.junit.Test; -import org.openmhealth.schema.pojos.HeartRate; -import org.openmhealth.schema.pojos.StepCount; -import org.openmhealth.shim.ShimDataResponse; -import org.openmhealth.shim.jawbone.JawboneShim; - -import java.io.IOException; -import java.io.InputStream; -import java.net.URL; -import java.util.List; -import java.util.Map; - -import static org.junit.Assert.assertNotNull; -import static org.junit.Assert.assertTrue; - -/** - * @author Danilo Bonilla - */ -public class FitbitShimTest { - - @Test - @SuppressWarnings("unchecked") - public void testNormalize() throws IOException { - URL url = Thread.currentThread().getContextClassLoader().getResource("fitbit-heart.json"); - assert url != null; - InputStream inputStream = url.openStream(); - - ObjectMapper objectMapper = new ObjectMapper(); - - FitbitShim.FitbitDataType.HEART.getNormalizer(); - SimpleModule module = new SimpleModule(); - module.addDeserializer(ShimDataResponse.class, - FitbitShim.FitbitDataType.HEART.getNormalizer()); - - objectMapper.registerModule(module); - - ShimDataResponse response = - objectMapper.readValue(inputStream, ShimDataResponse.class); - - assertNotNull(response); - - assertNotNull(response.getShim()); - - Map map = (Map) response.getBody(); - assertTrue(map.containsKey(HeartRate.SCHEMA_HEART_RATE)); - - List stepCounts = (List) map.get(HeartRate.SCHEMA_HEART_RATE); - assertTrue(stepCounts != null && stepCounts.size() == 6); - } -} diff --git a/shim-server/src/test/java/org/openmhealth/shim/fitbit/mapper/FitbitBodyMassIndexDataPointMapperUnitTests.java b/shim-server/src/test/java/org/openmhealth/shim/fitbit/mapper/FitbitBodyMassIndexDataPointMapperUnitTests.java new file mode 100644 index 00000000..3d3db83c --- /dev/null +++ b/shim-server/src/test/java/org/openmhealth/shim/fitbit/mapper/FitbitBodyMassIndexDataPointMapperUnitTests.java @@ -0,0 +1,65 @@ +package org.openmhealth.shim.fitbit.mapper; + +import com.fasterxml.jackson.databind.JsonNode; +import org.openmhealth.schema.domain.omh.BodyMassIndex; +import org.openmhealth.schema.domain.omh.BodyMassIndexUnit; +import org.openmhealth.schema.domain.omh.DataPoint; +import org.openmhealth.schema.domain.omh.TypedUnitValue; +import org.openmhealth.shim.common.mapper.DataPointMapperUnitTests; +import org.springframework.core.io.ClassPathResource; +import org.testng.annotations.BeforeTest; +import org.testng.annotations.Test; + +import java.io.IOException; +import java.time.OffsetDateTime; +import java.util.List; + +import static java.util.Collections.singletonList; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.equalTo; + +/** + * @author Chris Schaefbauer + */ +public class FitbitBodyMassIndexDataPointMapperUnitTests extends DataPointMapperUnitTests{ + + protected JsonNode responseNodeWeight; + + private final FitbitBodyMassIndexDataPointMapper mapper = new FitbitBodyMassIndexDataPointMapper(); + + @BeforeTest + public void initializeResponseNode() throws IOException { + + ClassPathResource resource = new ClassPathResource("org/openmhealth/shim/fitbit/mapper/fitbit-get-body-weight.json"); + responseNodeWeight = objectMapper.readTree(resource.getInputStream()); + } + + @Test + public void asDataPointsShouldReturnCorrectNumberOfDataPoints(){ + + List> dataPoints = mapper.asDataPoints(singletonList(responseNodeWeight)); + assertThat(dataPoints.size(),equalTo(4)); + } + + @Test + public void asDataPointsShouldReturnCorrectDataPoints() { + + List> dataPoints = mapper.asDataPoints(singletonList(responseNodeWeight)); + testFitbitBodyMassIndexDataPoint(dataPoints.get(0), 21.48, "2015-05-13T18:28:59Z", 1431541739000L); + testFitbitBodyMassIndexDataPoint(dataPoints.get(1), 21.17, "2015-05-14T11:51:57Z", 1431604317000L); + testFitbitBodyMassIndexDataPoint(dataPoints.get(2), 21.99, "2015-05-22T18:12:06Z", 1432318326000L); + testFitbitBodyMassIndexDataPoint(dataPoints.get(3), 21.65, "2015-05-24T15:15:25Z", 1432480525000L); + + } + + public void testFitbitBodyMassIndexDataPoint(DataPoint dataPoint, double bodyMassIndexValue, String timeString, long logId){ + + TypedUnitValue bmiValue = new TypedUnitValue(BodyMassIndexUnit.KILOGRAMS_PER_SQUARE_METER,bodyMassIndexValue); + BodyMassIndex expectedBodyMassIndex = new BodyMassIndex.Builder(bmiValue).setEffectiveTimeFrame(OffsetDateTime.parse(timeString)).build(); + assertThat(dataPoint.getBody(), equalTo(expectedBodyMassIndex)); + assertThat(dataPoint.getHeader().getBodySchemaId(), equalTo(BodyMassIndex.SCHEMA_ID)); + assertThat(dataPoint.getHeader().getAcquisitionProvenance().getAdditionalProperties().get("external_id"), equalTo(logId)); + assertThat(dataPoint.getHeader().getAcquisitionProvenance().getSourceName(), equalTo(FitbitDataPointMapper.RESOURCE_API_SOURCE_NAME)); + + } +} diff --git a/shim-server/src/test/java/org/openmhealth/shim/fitbit/mapper/FitbitBodyWeightDataPointMapperUnitTests.java b/shim-server/src/test/java/org/openmhealth/shim/fitbit/mapper/FitbitBodyWeightDataPointMapperUnitTests.java new file mode 100644 index 00000000..bf820d35 --- /dev/null +++ b/shim-server/src/test/java/org/openmhealth/shim/fitbit/mapper/FitbitBodyWeightDataPointMapperUnitTests.java @@ -0,0 +1,77 @@ +package org.openmhealth.shim.fitbit.mapper; + +import com.fasterxml.jackson.databind.JsonNode; +import org.openmhealth.schema.domain.omh.BodyWeight; +import org.openmhealth.schema.domain.omh.DataPoint; +import org.openmhealth.schema.domain.omh.MassUnit; +import org.openmhealth.schema.domain.omh.MassUnitValue; +import org.openmhealth.shim.common.mapper.DataPointMapperUnitTests; +import org.springframework.core.io.ClassPathResource; +import org.testng.annotations.BeforeTest; +import org.testng.annotations.Test; + +import java.io.IOException; +import java.time.OffsetDateTime; +import java.util.List; + +import static java.util.Collections.singletonList; +import static org.hamcrest.CoreMatchers.equalTo; +import static org.hamcrest.MatcherAssert.assertThat; + +/** + * @author Chris Schaefbauer + */ +public class FitbitBodyWeightDataPointMapperUnitTests extends DataPointMapperUnitTests { + + protected JsonNode responseNodeWeight; + + private final FitbitBodyWeightDataPointMapper mapper = new FitbitBodyWeightDataPointMapper(); + @BeforeTest + public void initializeResponseNode() throws IOException { + + ClassPathResource resource = new ClassPathResource("org/openmhealth/shim/fitbit/mapper/fitbit-get-body-weight.json"); + responseNodeWeight = objectMapper.readTree(resource.getInputStream()); + + } + + @Test + public void asDataPointsShouldReturnCorrectNumberOfDataPoints(){ + + List> bodyWeightDataPoints = mapper.asDataPoints(singletonList(responseNodeWeight)); + assertThat(bodyWeightDataPoints.size(),equalTo(4)); + + } + + @Test + public void asDataPointsShouldReturnCorrectDataPoints(){ + + List> bodyWeightDataPoints = mapper.asDataPoints(singletonList(responseNodeWeight)); + + testFitbitBodyWeightDataPoint(bodyWeightDataPoints.get(0),56.7,"2015-05-13T18:28:59Z",1431541739000L); + testFitbitBodyWeightDataPoint(bodyWeightDataPoints.get(1),55.9,"2015-05-14T11:51:57Z",1431604317000L); + testFitbitBodyWeightDataPoint(bodyWeightDataPoints.get(2),58.1,"2015-05-22T18:12:06Z",1432318326000L); + testFitbitBodyWeightDataPoint(bodyWeightDataPoints.get(3),57.2,"2015-05-24T15:15:25Z",1432480525000L); + + } + + @Test + public void asDataPointsShouldReturnEmptyListForEmptyArray() throws IOException { + + JsonNode emptyWeightNode = objectMapper.readTree("{\n" + + " \"weight\": []\n" + + "}"); + List> emptyDataPoints = mapper.asDataPoints(singletonList(emptyWeightNode)); + assertThat(emptyDataPoints.isEmpty(),equalTo(true)); + } + + public void testFitbitBodyWeightDataPoint(DataPoint dataPoint,double massValue,String timeString,long logId){ + + BodyWeight expectedBodyWeight = new BodyWeight.Builder(new MassUnitValue(MassUnit.KILOGRAM,massValue)).setEffectiveTimeFrame(OffsetDateTime.parse(timeString)).build(); + assertThat(dataPoint.getBody(),equalTo(expectedBodyWeight)); + assertThat(dataPoint.getHeader().getBodySchemaId(),equalTo(BodyWeight.SCHEMA_ID)); + assertThat(dataPoint.getHeader().getAcquisitionProvenance().getAdditionalProperties().get("external_id"),equalTo(logId)); + assertThat(dataPoint.getHeader().getAcquisitionProvenance().getSourceName(),equalTo(FitbitDataPointMapper.RESOURCE_API_SOURCE_NAME)); + + } + +} diff --git a/shim-server/src/test/java/org/openmhealth/shim/fitbit/mapper/FitbitPhysicalActivityDataPointMapperUnitTests.java b/shim-server/src/test/java/org/openmhealth/shim/fitbit/mapper/FitbitPhysicalActivityDataPointMapperUnitTests.java new file mode 100644 index 00000000..ed622934 --- /dev/null +++ b/shim-server/src/test/java/org/openmhealth/shim/fitbit/mapper/FitbitPhysicalActivityDataPointMapperUnitTests.java @@ -0,0 +1,97 @@ +package org.openmhealth.shim.fitbit.mapper; + +import com.fasterxml.jackson.databind.JsonNode; +import org.hamcrest.CoreMatchers; +import org.openmhealth.schema.domain.omh.*; +import org.openmhealth.shim.common.mapper.DataPointMapperUnitTests; +import org.springframework.core.io.ClassPathResource; +import org.testng.annotations.BeforeTest; +import org.testng.annotations.Test; + +import java.io.IOException; +import java.time.OffsetDateTime; +import java.time.format.DateTimeFormatter; +import java.util.List; + +import static java.util.Collections.singletonList; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.equalTo; + +/** + * @author Chris Schaefbauer + */ +public class FitbitPhysicalActivityDataPointMapperUnitTests extends DataPointMapperUnitTests { + + JsonNode responseNodeSingleActivity,responseNodeMultipleActivities,responseNodeEmptyActivities; + FitbitPhysicalActivityDataPointMapper mapper = new FitbitPhysicalActivityDataPointMapper(); + + @BeforeTest + public void initializeResponseNodes() throws IOException { + + ClassPathResource resource = new ClassPathResource("org/openmhealth/shim/fitbit/mapper/fitbit-get-activities-single.json"); + responseNodeSingleActivity = objectMapper.readTree(resource.getInputStream()); + resource = new ClassPathResource("org/openmhealth/shim/fitbit/mapper/fitbit-get-activities-multiple.json"); + responseNodeMultipleActivities = objectMapper.readTree(resource.getInputStream()); + resource = new ClassPathResource("org/openmhealth/shim/fitbit/mapper/fitbit-get-activities-empty.json"); + responseNodeEmptyActivities = objectMapper.readTree(resource.getInputStream()); + + } + + @Test + public void asDataPointsShouldReturnCorrectNumberOfDataPoints(){ + List> dataPoints = mapper.asDataPoints(singletonList(responseNodeSingleActivity)); + assertThat(dataPoints.size(),equalTo(1)); + + dataPoints = mapper.asDataPoints(singletonList(responseNodeMultipleActivities)); + assertThat(dataPoints.size(),equalTo(3)); + + dataPoints = mapper.asDataPoints(singletonList(responseNodeEmptyActivities)); + assertThat(dataPoints.size(),equalTo(0)); + } + + @Test + public void asDataPointsShouldReturnCorrectDataPointForSingleActivty(){ + List> dataPoints = mapper.asDataPoints(singletonList(responseNodeSingleActivity)); + testFitbitPhysicalActivityDataPoint(dataPoints.get(0),"Walk","2014-06-19","09:00",3.36,3600000L,79441095L); + + } + + @Test + public void asDataPointsShouldReturnCorrectDataPointsForMultipleActivities(){ + List> dataPoints = mapper.asDataPoints(singletonList(responseNodeMultipleActivities)); + testFitbitPhysicalActivityDataPoint(dataPoints.get(0),"Run","2015-06-23","11:55",6.43738,1440000L,253202765L); + testFitbitPhysicalActivityDataPoint(dataPoints.get(1),"Swimming","2015-06-23","10:00",null,null,253246706L); + testFitbitPhysicalActivityDataPoint(dataPoints.get(2),"Walk","2015-06-23",null,6.43738,null,253202766L); + + } + + public void testFitbitPhysicalActivityDataPoint(DataPoint dataPoint,String activityName,String dateString,String startTimeString, Double distance,Long durationInMillis,Long logId){ + + PhysicalActivity.Builder dataPointBuilderForExpected = new PhysicalActivity.Builder(activityName); + + if(startTimeString!=null && durationInMillis!=null){ + OffsetDateTime offsetStartDateTime = OffsetDateTime.parse(dateString + "T" + startTimeString + "Z", DateTimeFormatter.ISO_OFFSET_DATE_TIME); + dataPointBuilderForExpected.setEffectiveTimeFrame(TimeInterval.ofStartDateTimeAndDuration(offsetStartDateTime,new DurationUnitValue(DurationUnit.MILLISECOND,durationInMillis))); + } + else if(startTimeString !=null){ + OffsetDateTime offsetStartDateTime = OffsetDateTime.parse(dateString + "T" + startTimeString + "Z",DateTimeFormatter.ISO_OFFSET_DATE_TIME); + dataPointBuilderForExpected.setEffectiveTimeFrame(offsetStartDateTime); + } + else{ + OffsetDateTime offsetStartDateTime = OffsetDateTime.parse(dateString + "T" + "00:00:00Z",DateTimeFormatter.ISO_OFFSET_DATE_TIME); + dataPointBuilderForExpected.setEffectiveTimeFrame(TimeInterval.ofStartDateTimeAndDuration(offsetStartDateTime, new DurationUnitValue(DurationUnit.DAY,1))); + } + + if(distance!=null){ + dataPointBuilderForExpected.setDistance(new LengthUnitValue(LengthUnit.KILOMETER,distance)); + } + PhysicalActivity expectedPhysicalActivity = dataPointBuilderForExpected.build(); + + assertThat(dataPoint.getBody(), CoreMatchers.equalTo(expectedPhysicalActivity)); + assertThat(dataPoint.getHeader().getBodySchemaId(), CoreMatchers.equalTo(PhysicalActivity.SCHEMA_ID)); + assertThat(dataPoint.getHeader().getAcquisitionProvenance().getAdditionalProperties().get("external_id"), equalTo(logId)); + assertThat(dataPoint.getHeader().getAcquisitionProvenance().getSourceName(), CoreMatchers.equalTo(FitbitDataPointMapper.RESOURCE_API_SOURCE_NAME)); + + } + +} 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 new file mode 100644 index 00000000..6e24efe4 --- /dev/null +++ b/shim-server/src/test/java/org/openmhealth/shim/fitbit/mapper/FitbitSleepDurationDataPointMapperUnitTests.java @@ -0,0 +1,81 @@ +package org.openmhealth.shim.fitbit.mapper; + +import com.fasterxml.jackson.databind.JsonNode; +import org.openmhealth.schema.domain.omh.*; +import org.openmhealth.shim.common.mapper.DataPointMapperUnitTests; +import org.springframework.core.io.ClassPathResource; +import org.testng.annotations.BeforeTest; +import org.testng.annotations.Test; + +import java.io.IOException; +import java.time.OffsetDateTime; +import java.util.List; + +import static java.util.Collections.singletonList; +import static org.hamcrest.CoreMatchers.equalTo; +import static org.hamcrest.MatcherAssert.assertThat; + +/** + * @author Chris Schaefbauer + */ +public class FitbitSleepDurationDataPointMapperUnitTests extends DataPointMapperUnitTests { + + JsonNode responseNodeUserInfo,responseNodeSleep,responseNodeMultipleSleep; + protected FitbitSleepDurationDataPointMapper mapper = new FitbitSleepDurationDataPointMapper(); + + @BeforeTest + public void initializeResponseNode() throws IOException { + + ClassPathResource resource = new ClassPathResource("org/openmhealth/shim/fitbit/mapper/fitbit-get-sleep.json"); + responseNodeSleep = objectMapper.readTree(resource.getInputStream()); + + resource = new ClassPathResource("org/openmhealth/shim/fitbit/mapper/fitbit-get-user-info.json"); + responseNodeUserInfo = objectMapper.readTree(resource.getInputStream()); + + resource = new ClassPathResource("org/openmhealth/shim/fitbit/mapper/fitbit-get-sleep-multiple.json"); + responseNodeMultipleSleep = objectMapper.readTree(resource.getInputStream()); + } + + @Test + public void asDataPointsShouldReturnCorrectNumberOfDataPoints(){ + + List> dataPoints = mapper.asDataPoints(singletonList(responseNodeSleep)); + assertThat(dataPoints.size(),equalTo(1)); + + dataPoints = mapper.asDataPoints(singletonList(responseNodeMultipleSleep)); + assertThat(dataPoints.size(),equalTo(2)); + + } + + @Test + public void asDataPointsShouldReturnCorrectDataPoints(){ + + List> dataPoints = mapper.asDataPoints(singletonList(responseNodeSleep)); + + SleepDuration.Builder expectedSleepDurationBuilder = new SleepDuration.Builder(new DurationUnitValue(DurationUnit.MINUTE,831)); + OffsetDateTime offsetStartDateTime = OffsetDateTime.parse("2014-07-19T11:58:00Z"); + expectedSleepDurationBuilder.setEffectiveTimeFrame(TimeInterval + .ofStartDateTimeAndDuration(offsetStartDateTime, new DurationUnitValue(DurationUnit.MINUTE, 961))); + + SleepDuration expectedSleepDuration = expectedSleepDurationBuilder.build(); + + SleepDuration body = dataPoints.get(0).getBody(); + assertThat(body,equalTo(expectedSleepDuration)); + + + } + + @Test + public void asDataPointsShouldReturnEmptyListWhenResponseIsEmpty() throws IOException { + + ClassPathResource resource = new ClassPathResource("org/openmhealth/shim/fitbit/mapper/fitbit-get-sleep-empty.json"); + JsonNode responseNodeEmptySleep = objectMapper.readTree(resource.getInputStream()); + + List> dataPoints = mapper.asDataPoints(singletonList(responseNodeEmptySleep)); + + assertThat(dataPoints.size(),equalTo(0)); + + + } + +} diff --git a/shim-server/src/test/java/org/openmhealth/shim/fitbit/mapper/FitbitStepCountDataPointUnitTests.java b/shim-server/src/test/java/org/openmhealth/shim/fitbit/mapper/FitbitStepCountDataPointUnitTests.java new file mode 100644 index 00000000..25853802 --- /dev/null +++ b/shim-server/src/test/java/org/openmhealth/shim/fitbit/mapper/FitbitStepCountDataPointUnitTests.java @@ -0,0 +1,93 @@ +package org.openmhealth.shim.fitbit.mapper; + +import com.fasterxml.jackson.databind.JsonNode; +import org.hamcrest.CoreMatchers; +import org.openmhealth.schema.domain.omh.*; +import org.openmhealth.shim.common.mapper.DataPointMapperUnitTests; +import org.springframework.core.io.ClassPathResource; +import org.testng.annotations.BeforeTest; +import org.testng.annotations.Test; + +import java.io.IOException; +import java.time.OffsetDateTime; +import java.time.format.DateTimeFormatter; +import java.util.List; + +import static java.util.Collections.singletonList; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.equalTo; + + +/** + * @author Chris Schaefbauer + */ +public class FitbitStepCountDataPointUnitTests extends DataPointMapperUnitTests { + + JsonNode responseNodeStepTimeSeries; + protected final FitbitStepCountDataPointMapper mapper = new FitbitStepCountDataPointMapper(); + + @BeforeTest + public void initializeResponseNodes() throws IOException { + + ClassPathResource resource = + new ClassPathResource("org/openmhealth/shim/fitbit/mapper/fitbit-time-series-steps.json"); + responseNodeStepTimeSeries = objectMapper.readTree(resource.getInputStream()); + + } + + @Test + public void asDataPointsShouldReturnCorrectNumberOfDataPoints() { + + List> stepDataPoints = mapper.asDataPoints(singletonList(responseNodeStepTimeSeries)); + assertThat(stepDataPoints.size(), equalTo(2)); + } + + @Test + public void asDataPointsShouldReturnCorrectDataPoints() { + + List> stepDataPoints = mapper.asDataPoints(singletonList(responseNodeStepTimeSeries)); + + testFitbitStepCountDataPoint(stepDataPoints.get(0), 2170, "2015-05-26"); + testFitbitStepCountDataPoint(stepDataPoints.get(1), 3248, "2015-05-27"); + } + + @Test + public void asDataPointsShouldReturnNoDataPointWhenStepCountEqualsZero() throws IOException { + + JsonNode zeroStepsNode = objectMapper.readTree( + "{\n" + + "\"activities-steps\": [ \n" + + "{\n" + + "\"dateTime\": \"2015-05-24\"\n," + + "\"value\": \"0\"\n" + + "}\n" + + "]\n" + + "}"); + + List> dataPoints = mapper.asDataPoints(singletonList(zeroStepsNode)); + assertThat(dataPoints.size(),equalTo(0)); + + } + + public void testFitbitStepCountDataPoint(DataPoint dataPoint, long stepCount, String dateString) { + + StepCount.Builder dataPointBuilderForExpected = new StepCount.Builder(stepCount); + + OffsetDateTime startDateTime = + OffsetDateTime.parse(dateString + "T" + "00:00:00Z", DateTimeFormatter.ISO_OFFSET_DATE_TIME); + startDateTime = startDateTime.withNano(000000000); + + dataPointBuilderForExpected.setEffectiveTimeFrame( + TimeInterval.ofStartDateTimeAndDuration(startDateTime, new DurationUnitValue(DurationUnit.DAY, 1))); + StepCount expectedStepCount = dataPointBuilderForExpected.build(); + + assertThat(dataPoint.getBody(), CoreMatchers.equalTo(expectedStepCount)); + assertThat(dataPoint.getHeader().getBodySchemaId(), CoreMatchers.equalTo(StepCount.SCHEMA_ID)); + assertThat(dataPoint.getHeader().getAcquisitionProvenance().getAdditionalProperties().get("external_id"), + CoreMatchers.nullValue()); + assertThat(dataPoint.getHeader().getAcquisitionProvenance().getSourceName(), + CoreMatchers.equalTo(FitbitDataPointMapper.RESOURCE_API_SOURCE_NAME)); + + } + +} diff --git a/shim-server/src/test/java/org/openmhealth/shim/googlefit/mapper/GoogleFitBodyHeightDataPointMapperUnitTests.java b/shim-server/src/test/java/org/openmhealth/shim/googlefit/mapper/GoogleFitBodyHeightDataPointMapperUnitTests.java new file mode 100644 index 00000000..99a8e1b1 --- /dev/null +++ b/shim-server/src/test/java/org/openmhealth/shim/googlefit/mapper/GoogleFitBodyHeightDataPointMapperUnitTests.java @@ -0,0 +1,90 @@ +package org.openmhealth.shim.googlefit.mapper; + +import org.openmhealth.schema.domain.omh.*; +import org.springframework.core.io.ClassPathResource; +import org.testng.annotations.BeforeTest; +import org.testng.annotations.Test; + +import java.io.IOException; +import java.time.OffsetDateTime; +import java.util.List; +import java.util.Map; + +import static java.util.Collections.singletonList; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.equalTo; +import static org.openmhealth.schema.domain.omh.DataPointModality.SELF_REPORTED; +import static org.openmhealth.schema.domain.omh.LengthUnit.METER; + + +/** + * @author Chris Schaefbauer + */ +public class GoogleFitBodyHeightDataPointMapperUnitTests extends GoogleFitDataPointMapperUnitTests { + + private GoogleFitBodyHeightDataPointMapper mapper = new GoogleFitBodyHeightDataPointMapper(); + + + @BeforeTest + @Override + public void initializeResponseNode() throws IOException { + + ClassPathResource resource = + new ClassPathResource("org/openmhealth/shim/googlefit/mapper/googlefit-body-height.json"); + responseNode = objectMapper.readTree(resource.getInputStream()); + } + + @Test + public void asDataPointsShouldReturnCorrectNumberOfDataPoints() { + + List> dataPoints = mapper.asDataPoints(singletonList(responseNode)); + assertThat(dataPoints.size(), equalTo(2)); + } + + @Test + public void asDataPointsShouldReturnCorrectDataPointForSingleTimePoint() { + + List> dataPoints = mapper.asDataPoints(singletonList(responseNode)); + testGoogleFitDataPoint(dataPoints.get(0), + createFloatingPointTestProperties(1.8287990093231201, "2015-07-08T03:17:06.030Z", null, + "raw:com.google.height:com.google.android.apps.fitness:user_input")); + } + + @Test + public void asDataPointsShouldReturnCorrectDataPointForTimeRange() { + + List> dataPoints = mapper.asDataPoints(singletonList(responseNode)); + testGoogleFitDataPoint(dataPoints.get(1), + createFloatingPointTestProperties(1.828800082206726, "2015-07-08T14:43:57.544Z", + "2015-07-08T14:43:58.545Z", + "raw:com.google.height:com.google.android.apps.fitness:user_input")); + } + + @Test + public void asDataPointsShouldReturnSelfReportedAsModalityWhenDataSourceContainsUserInput() { + + List> dataPoints = mapper.asDataPoints(singletonList(responseNode)); + + assertThat(dataPoints.get(1).getHeader().getAcquisitionProvenance().getModality(), equalTo(SELF_REPORTED)); + } + + /* Helper methods */ + // TODO clean up + @Override + public void testGoogleFitMeasureFromDataPoint(BodyHeight testMeasure, Map properties) { + + BodyHeight.Builder bodyHeightBuilder = + new BodyHeight.Builder(new LengthUnitValue(METER, (double) properties.get("fpValue"))); + if (properties.containsKey("endDateTimeString")) { + bodyHeightBuilder.setEffectiveTimeFrame(TimeInterval.ofStartDateTimeAndEndDateTime( + OffsetDateTime.parse((String) properties.get("startDateTimeString")), + OffsetDateTime.parse((String) properties.get("endDateTimeString")))); + } + else { + bodyHeightBuilder + .setEffectiveTimeFrame(OffsetDateTime.parse((String) properties.get("startDateTimeString"))); + } + BodyHeight expectedBodyHeight = bodyHeightBuilder.build(); + assertThat(testMeasure, equalTo(expectedBodyHeight)); + } +} diff --git a/shim-server/src/test/java/org/openmhealth/shim/googlefit/mapper/GoogleFitBodyWeightDataPointMapperUnitTests.java b/shim-server/src/test/java/org/openmhealth/shim/googlefit/mapper/GoogleFitBodyWeightDataPointMapperUnitTests.java new file mode 100644 index 00000000..744288e2 --- /dev/null +++ b/shim-server/src/test/java/org/openmhealth/shim/googlefit/mapper/GoogleFitBodyWeightDataPointMapperUnitTests.java @@ -0,0 +1,84 @@ +package org.openmhealth.shim.googlefit.mapper; + +import org.openmhealth.schema.domain.omh.*; +import org.springframework.core.io.ClassPathResource; +import org.testng.annotations.BeforeTest; +import org.testng.annotations.Test; + +import java.io.IOException; +import java.util.List; +import java.util.Map; + +import static java.util.Collections.singletonList; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.nullValue; +import static org.openmhealth.schema.domain.omh.DataPointModality.SELF_REPORTED; + + +/** + * @author Chris Schaefbauer + */ +public class GoogleFitBodyWeightDataPointMapperUnitTests extends GoogleFitDataPointMapperUnitTests { + + private GoogleFitBodyWeightDataPointMapper mapper = new GoogleFitBodyWeightDataPointMapper(); + + + @BeforeTest + @Override + public void initializeResponseNode() throws IOException { + ClassPathResource resource = + new ClassPathResource("org/openmhealth/shim/googlefit/mapper/googlefit-body-weight.json"); + responseNode = objectMapper.readTree(resource.getInputStream()); + } + + @Test + public void asDataPointsShouldReturnCorrectNumberOfDataPoints() { + List> dataPoints = mapper.asDataPoints(singletonList(responseNode)); + assertThat(dataPoints.size(), equalTo(3)); + } + + @Test + public void asDataPointsShouldReturnCorrectDataPointsForSingleTimePoint() { + + List> dataPoints = mapper.asDataPoints(singletonList(responseNode)); + testGoogleFitDataPoint(dataPoints.get(0), + createFloatingPointTestProperties(72.0999984741211, "2015-02-13T00:00:00Z", null, + "raw:com.google.weight:com.fatsecret.android:")); + testGoogleFitDataPoint(dataPoints.get(1), + createFloatingPointTestProperties(72, "2015-02-17T16:57:13.313Z", null, + "raw:com.google.weight:com.wsl.noom:")); + } + + @Test + public void asDataPointsShouldReturnCorrectDataPointsWithTimeRange() { + + List> dataPoints = mapper.asDataPoints(singletonList(responseNode)); + + testGoogleFitDataPoint(dataPoints.get(2), + createFloatingPointTestProperties(75.75070190429688, "2015-07-08T03:17:00Z", "2015-07-08T03:17:10.020Z", + "raw:com.google.weight:com.google.android.apps.fitness:user_input")); + } + + @Test + public void asDataPointsShouldReturnSelfReportedAsModalityWhenDataSourceContainsUserInput() { + + List> dataPoints = mapper.asDataPoints(singletonList(responseNode)); + + // TODO split + assertThat(dataPoints.get(2).getHeader().getAcquisitionProvenance().getModality(), equalTo(SELF_REPORTED)); + assertThat(dataPoints.get(0).getHeader().getAcquisitionProvenance().getModality(), nullValue()); + + } + + /* Helper methods */ + // TODO clean up + @Override + public void testGoogleFitMeasureFromDataPoint(BodyWeight testMeasure, Map properties) { + BodyWeight.Builder expectedBodyWeightBuilder = + new BodyWeight.Builder(new MassUnitValue(MassUnit.KILOGRAM, (Double) properties.get("fpValue"))); + setExpectedEffectiveTimeFrame(expectedBodyWeightBuilder, properties); + BodyWeight expectedBodyWeight = expectedBodyWeightBuilder.build(); + assertThat(testMeasure, equalTo(expectedBodyWeight)); + } +} diff --git a/shim-server/src/test/java/org/openmhealth/shim/googlefit/mapper/GoogleFitCaloriesBurnedDataPointMapperUnitTests.java b/shim-server/src/test/java/org/openmhealth/shim/googlefit/mapper/GoogleFitCaloriesBurnedDataPointMapperUnitTests.java new file mode 100644 index 00000000..c9d07f2a --- /dev/null +++ b/shim-server/src/test/java/org/openmhealth/shim/googlefit/mapper/GoogleFitCaloriesBurnedDataPointMapperUnitTests.java @@ -0,0 +1,82 @@ +package org.openmhealth.shim.googlefit.mapper; + +import org.openmhealth.schema.domain.omh.*; +import org.springframework.core.io.ClassPathResource; +import org.testng.annotations.BeforeTest; +import org.testng.annotations.Test; + +import java.io.IOException; +import java.util.List; +import java.util.Map; + +import static java.util.Collections.singletonList; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.nullValue; + + +/** + * @author Chris Schaefbauer + */ +public class GoogleFitCaloriesBurnedDataPointMapperUnitTests extends GoogleFitDataPointMapperUnitTests { + + private GoogleFitCaloriesBurnedDataPointMapper mapper = new GoogleFitCaloriesBurnedDataPointMapper(); + + @BeforeTest + @Override + public void initializeResponseNode() throws IOException { + + ClassPathResource resource = + new ClassPathResource("org/openmhealth/shim/googlefit/mapper/googlefit-calories-burned.json"); + responseNode = objectMapper.readTree(resource.getInputStream()); + } + + @Test + public void asDataPointsShouldReturnCorrectNumberOfDataPoints() { + + List> dataPoints = mapper.asDataPoints(singletonList(responseNode)); + + assertThat(dataPoints.size(), equalTo(2)); + } + + @Test + public void asDataPointsShouldReturnCorrectDataPoints() { + + List> dataPoints = mapper.asDataPoints(singletonList(responseNode)); + + testGoogleFitDataPoint(dataPoints.get(0), + createFloatingPointTestProperties(200.0, "2015-07-07T13:30:00Z", "2015-07-07T14:00:00Z", + "raw:com.google.calories.expended:com.google.android.apps.fitness:user_input")); + testGoogleFitDataPoint(dataPoints.get(1), + createFloatingPointTestProperties(4.221510410308838, "2015-07-08T14:43:49.730Z", + "2015-07-08T14:47:27.809Z", + "derived:com.google.calories.expended:com.google.android.gms:from_activities")); + } + + @Test + public void asDataPointsShouldReturnSelfReportedAsModalityWhenDataSourceContainsUserInput() { + + List> dataPoints = mapper.asDataPoints(singletonList(responseNode)); + + assertThat(dataPoints.get(0).getHeader().getAcquisitionProvenance().getModality(), + equalTo(DataPointModality.SELF_REPORTED)); + + assertThat(dataPoints.get(1).getHeader().getAcquisitionProvenance().getModality(), nullValue()); + + } + + + /* Helper methods */ + + @Override + public void testGoogleFitMeasureFromDataPoint(CaloriesBurned testMeasure, Map properties) { + + CaloriesBurned.Builder expectedCaloriesBurnedBuilder = + new CaloriesBurned.Builder(new KcalUnitValue(KcalUnit.KILOCALORIE, (double) properties.get("fpValue"))); + setExpectedEffectiveTimeFrame(expectedCaloriesBurnedBuilder, properties); + + CaloriesBurned expectedCaloriesBurned = expectedCaloriesBurnedBuilder.build(); + + assertThat(testMeasure, equalTo(expectedCaloriesBurned)); + } +} diff --git a/shim-server/src/test/java/org/openmhealth/shim/googlefit/mapper/GoogleFitDataPointMapperUnitTests.java b/shim-server/src/test/java/org/openmhealth/shim/googlefit/mapper/GoogleFitDataPointMapperUnitTests.java new file mode 100644 index 00000000..ffa13f2d --- /dev/null +++ b/shim-server/src/test/java/org/openmhealth/shim/googlefit/mapper/GoogleFitDataPointMapperUnitTests.java @@ -0,0 +1,153 @@ +package org.openmhealth.shim.googlefit.mapper; + +import com.fasterxml.jackson.databind.JsonNode; +import com.google.common.collect.Maps; +import org.openmhealth.schema.domain.omh.*; +import org.openmhealth.shim.common.mapper.DataPointMapperUnitTests; + +import java.io.IOException; +import java.time.OffsetDateTime; +import java.util.HashMap; +import java.util.Map; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.nullValue; +import static org.openmhealth.shim.googlefit.mapper.GoogleFitDataPointMapper.RESOURCE_API_SOURCE_NAME; + + +/** + * Base class for unit tests that evaluate individual data point mappers, used to build the measure specific unit + * tests. + * + * @author Chris Schaefbauer + */ +public abstract class GoogleFitDataPointMapperUnitTests extends DataPointMapperUnitTests { + + protected JsonNode responseNode; + + public abstract void initializeResponseNode() throws IOException; + + /** + * Implemented by measure specific test classes in order to test the {@link Measure} contained within the mapper + * created {@link DataPoint}. Should contain the assertions needed to test the individual values in the measure. + */ + public abstract void testGoogleFitMeasureFromDataPoint(T testMeasure, Map properties); + + /** + * Used to test data points created through {@link Measure} specific Google Fit mappers. + * + * @param dataPoint datapoint created by the mapper + * @param properties a map containing different properties to test against the mapper generated datapoint, should + * contain keys that are used in this generic data point test as well as the mapper specific test + */ + public void testGoogleFitDataPoint(DataPoint dataPoint, Map properties) { + + testGoogleFitMeasureFromDataPoint(dataPoint.getBody(), properties); + DataPointHeader dataPointHeader = dataPoint.getHeader(); + + assertThat(dataPointHeader.getAcquisitionProvenance().getSourceName(), equalTo(RESOURCE_API_SOURCE_NAME)); + + assertThat(dataPointHeader.getAcquisitionProvenance().getAdditionalProperties().get( + "source_origin_id"), equalTo(properties.get("sourceOriginId"))); + + if (properties.containsKey("modality")) { + assertThat(dataPointHeader.getAcquisitionProvenance().getModality(), equalTo(properties.get("modality"))); + } + + if (!properties.containsKey("modality")) { + assertThat(dataPointHeader.getAcquisitionProvenance().getModality(), nullValue()); + } + + } + + /** + * Creates the properties map used for generating an expected values datapoint to test google fit data points + * against. + * + * @param fpValue a floating point value from a Google fit JSON test datapoint + * @param startDateTime a string containing the start timestamp in unix epoch nanoseconds + * @param endDateTime a string containing the end timestamp in unix epoch nanoseconds + * @param sourceOriginId a string containing the origin source id from the datapoint from the JSON test data + * @return a map with the properties needed to generate an expected datapoint to test a google fit datapoint + */ + public Map createFloatingPointTestProperties(double fpValue, String startDateTime, + String endDateTime, String sourceOriginId) { + Map properties = createTestProperties(startDateTime, endDateTime, sourceOriginId); + properties.put("fpValue", fpValue); + return properties; + } + + /** + * Creates the properties map used for generating an expected values datapoint to test google fit data points + * against. + * + * @param intValue an integer value from a Google fit JSON test datapoint + * @param startDateTime a string containing the start timestamp in unix epoch nanoseconds + * @param endDateTime a string containing the end timestamp in unix epoch nanoseconds + * @param sourceOriginId a string containing the origin source id from the datapoint from the JSON test data + * @return a map with the properties needed to generate an expected datapoint to test a google fit datapoint + */ + public Map createIntegerTestProperties(long intValue, String startDateTime, String endDateTime, + String sourceOriginId) { + + Map properties = createTestProperties(startDateTime, endDateTime, sourceOriginId); + properties.put("intValue", intValue); + return properties; + } + + /** + * Creates the properties map used for generating an expected values datapoint to test google fit data points + * against, used specifically for testing physical activity because it generates a string value as the activity + * type. + * + * @param stringValue an string value from a Google fit JSON test datapoint + * @param startDateTime a string containing the start timestamp in unix epoch nanoseconds + * @param endDateTime a string containing the end timestamp in unix epoch nanoseconds + * @param sourceOriginId a string containing the origin source id from the datapoint from the JSON test data + * @return a map with the properties needed to generate an expected datapoint to test a google fit datapoint + */ + public Map createStringTestProperties(String stringValue, String startDateTime, String endDateTime, + String sourceOriginId) { + + Map properties = createTestProperties(startDateTime, endDateTime, sourceOriginId); + properties.put("stringValue", stringValue); + return properties; + } + + private Map createTestProperties(String startDateTimeString, String endDateTimeString, + String sourceOriginId) { + + HashMap properties = Maps.newHashMap(); + if (startDateTimeString != null) { + properties.put("startDateTimeString", startDateTimeString); + } + if (endDateTimeString != null) { + properties.put("endDateTimeString", endDateTimeString); + } + if (sourceOriginId != null) { + properties.put("sourceOriginId", sourceOriginId); + if (sourceOriginId.endsWith("user_input")) { + properties.put("modality", DataPointModality.SELF_REPORTED); + } + } + + return properties; + } + + /** + * Sets the effective time frame for a datapoint builder given a map of properties that contains the key + * "startDateTimeString" and optionally, "endDateTimeString". + */ + public void setExpectedEffectiveTimeFrame(T.Builder builder, Map properties) { + + if (properties.containsKey("endDateTimeString")) { + builder.setEffectiveTimeFrame(TimeInterval.ofStartDateTimeAndEndDateTime( + OffsetDateTime.parse((String) properties.get("startDateTimeString")), + OffsetDateTime.parse((String) properties.get("endDateTimeString")))); + } + else { + builder.setEffectiveTimeFrame(OffsetDateTime.parse((String) properties.get("startDateTimeString"))); + } + } +} diff --git a/shim-server/src/test/java/org/openmhealth/shim/googlefit/mapper/GoogleFitHeartRateDataPointMapperUnitTests.java b/shim-server/src/test/java/org/openmhealth/shim/googlefit/mapper/GoogleFitHeartRateDataPointMapperUnitTests.java new file mode 100644 index 00000000..422fe419 --- /dev/null +++ b/shim-server/src/test/java/org/openmhealth/shim/googlefit/mapper/GoogleFitHeartRateDataPointMapperUnitTests.java @@ -0,0 +1,71 @@ +package org.openmhealth.shim.googlefit.mapper; + +import org.openmhealth.schema.domain.omh.DataPoint; +import org.openmhealth.schema.domain.omh.HeartRate; +import org.springframework.core.io.ClassPathResource; +import org.testng.annotations.BeforeTest; +import org.testng.annotations.Test; + +import java.io.IOException; +import java.util.List; +import java.util.Map; + +import static java.util.Collections.singletonList; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.equalTo; + + +/** + * @author Chris Schaefbauer + */ +public class GoogleFitHeartRateDataPointMapperUnitTests extends GoogleFitDataPointMapperUnitTests { + + private GoogleFitHeartRateDataPointMapper mapper = new GoogleFitHeartRateDataPointMapper(); + + + @BeforeTest + @Override + public void initializeResponseNode() throws IOException { + + ClassPathResource resource = + new ClassPathResource("org/openmhealth/shim/googlefit/mapper/googlefit-heart-rate.json"); + responseNode = objectMapper.readTree(resource.getInputStream()); + } + + @Test + public void asDataPointsShouldReturnCorrectNumberOfDataPoints() { + + List> dataPoints = mapper.asDataPoints(singletonList(responseNode)); + assertThat(dataPoints.size(), equalTo(2)); + } + + @Test + public void asDataPointsShouldReturnCorrectDataPointsForSingleTimePoint() { + + List> dataPoints = mapper.asDataPoints(singletonList(responseNode)); + testGoogleFitDataPoint(dataPoints.get(0), + createFloatingPointTestProperties(54, "2015-01-30T15:37:48.186Z", null, + "raw:com.google.heart_rate.bpm:com.azumio.instantheartrate.full:")); + } + + @Test + public void asDataPointsShouldReturnCorrectDataPointsForTimeRange() { + + List> dataPoints = mapper.asDataPoints(singletonList(responseNode)); + testGoogleFitDataPoint(dataPoints.get(1), + createFloatingPointTestProperties(58.0, "2015-07-10T14:34:32.914Z", "2015-07-10T14:34:33.915Z", + "raw:com.google.heart_rate.bpm:si.modula.android.instantheartrate:")); + } + + /* Helper methods */ + // TODO clean up + @Override + public void testGoogleFitMeasureFromDataPoint(HeartRate testMeasure, Map properties) { + + HeartRate.Builder expectedHeartRateBuilder = new HeartRate.Builder((double) properties.get("fpValue")); + setExpectedEffectiveTimeFrame(expectedHeartRateBuilder, properties); + HeartRate expectedHeartRate = expectedHeartRateBuilder.build(); + + assertThat(testMeasure, equalTo(expectedHeartRate)); + } +} diff --git a/shim-server/src/test/java/org/openmhealth/shim/googlefit/mapper/GoogleFitPhysicalActivityDataPointMapperUnitTests.java b/shim-server/src/test/java/org/openmhealth/shim/googlefit/mapper/GoogleFitPhysicalActivityDataPointMapperUnitTests.java new file mode 100644 index 00000000..09d9dc21 --- /dev/null +++ b/shim-server/src/test/java/org/openmhealth/shim/googlefit/mapper/GoogleFitPhysicalActivityDataPointMapperUnitTests.java @@ -0,0 +1,105 @@ +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.DataPointModality; +import org.openmhealth.schema.domain.omh.PhysicalActivity; +import org.springframework.core.io.ClassPathResource; +import org.testng.annotations.BeforeTest; +import org.testng.annotations.Test; + +import java.io.IOException; +import java.util.List; +import java.util.Map; + +import static java.util.Collections.singletonList; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.nullValue; + + +/** + * @author Chris Schaefbauer + */ +public class GoogleFitPhysicalActivityDataPointMapperUnitTests + extends GoogleFitDataPointMapperUnitTests { + + private GoogleFitPhysicalActivityDataPointMapper mapper = new GoogleFitPhysicalActivityDataPointMapper(); + protected JsonNode sleepActivityNode; + + @BeforeTest + @Override + public void initializeResponseNode() throws IOException { + + ClassPathResource resource = + new ClassPathResource("org/openmhealth/shim/googlefit/mapper/googlefit-physical-activity.json"); + responseNode = objectMapper.readTree(resource.getInputStream()); + resource = new ClassPathResource("org/openmhealth/shim/googlefit/mapper/googlefit-sleep-activity.json"); + sleepActivityNode = objectMapper.readTree(resource.getInputStream()); + + + } + + @Test + public void asDataPointsShouldReturnCorrectNumberOfDataPoints() { + + List> dataPoints = mapper.asDataPoints(singletonList(responseNode)); + assertThat(dataPoints.size(), equalTo(3)); + } + + @Test + public void asDataPointsShouldReturnCorrectDataPoints() { + + List> dataPoints = mapper.asDataPoints(singletonList(responseNode)); + testGoogleFitDataPoint(dataPoints.get(0), + createStringTestProperties("Walking", "2015-01-01T22:21:57Z", "2015-01-01T23:29:49Z", + "derived:com.google.activity.segment:com.strava:session_activity_segment")); + testGoogleFitDataPoint(dataPoints.get(1), + createStringTestProperties("Aerobics", "2015-01-05T00:27:29.151Z", "2015-01-05T00:30:41.151Z", + "derived:com.google.activity.segment:com.mapmyrun.android2:session_activity_segment")); + + } + + @Test + public void asDataPointsShouldReturnSelfReportedAsModalityWhenDataSourceContainsUserInput() { + + List> dataPoints = mapper.asDataPoints(singletonList(responseNode)); + + assertThat(dataPoints.get(2).getHeader().getAcquisitionProvenance().getModality(), + equalTo(DataPointModality.SELF_REPORTED)); + + assertThat(dataPoints.get(1).getHeader().getAcquisitionProvenance().getModality(), nullValue()); + + } + + @Test + public void asDataPointsShouldNotReturnDataPointsForSleepActivityType() { + + List> dataPoints = mapper.asDataPoints(singletonList(sleepActivityNode)); + assertThat(dataPoints.size(), equalTo(0)); + } + + @Test + public void asDataPointsShouldNotReturnDataPointsForStationaryActivityTypes() throws IOException { + + ClassPathResource resource = + new ClassPathResource("org/openmhealth/shim/googlefit/mapper/googlefit-stationary-activity.json"); + JsonNode stationaryActivityNode = objectMapper.readTree(resource.getInputStream()); + + List> dataPoints = mapper.asDataPoints(singletonList(stationaryActivityNode)); + assertThat(dataPoints.size(),equalTo(0)); + } + + /* Helper methods */ + + @Override + public void testGoogleFitMeasureFromDataPoint(PhysicalActivity testMeasure, Map properties) { + + PhysicalActivity.Builder physicalActivityBuilder = + new PhysicalActivity.Builder((String) properties.get("stringValue")); + setExpectedEffectiveTimeFrame(physicalActivityBuilder, properties); + PhysicalActivity expectedPhysicalActivity = physicalActivityBuilder.build(); + + assertThat(testMeasure, equalTo(expectedPhysicalActivity)); + } +} diff --git a/shim-server/src/test/java/org/openmhealth/shim/googlefit/mapper/GoogleFitStepCountDataPointMapperUnitTests.java b/shim-server/src/test/java/org/openmhealth/shim/googlefit/mapper/GoogleFitStepCountDataPointMapperUnitTests.java new file mode 100644 index 00000000..ecb8c04c --- /dev/null +++ b/shim-server/src/test/java/org/openmhealth/shim/googlefit/mapper/GoogleFitStepCountDataPointMapperUnitTests.java @@ -0,0 +1,91 @@ +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.DataPointModality; +import org.openmhealth.schema.domain.omh.StepCount; +import org.springframework.core.io.ClassPathResource; +import org.testng.annotations.BeforeTest; +import org.testng.annotations.Test; + +import java.io.IOException; +import java.util.List; +import java.util.Map; + +import static java.util.Collections.singletonList; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.nullValue; + + +/** + * @author Chris Schaefbauer + */ +public class GoogleFitStepCountDataPointMapperUnitTests extends GoogleFitDataPointMapperUnitTests { + + private GoogleFitStepCountDataPointMapper mapper = new GoogleFitStepCountDataPointMapper(); + JsonNode emptyNode; + + @BeforeTest + @Override + public void initializeResponseNode() throws IOException { + + ClassPathResource resource = + new ClassPathResource("org/openmhealth/shim/googlefit/mapper/googlefit-step-count.json"); + responseNode = objectMapper.readTree(resource.getInputStream()); + resource = new ClassPathResource("org/openmhealth/shim/googlefit/mapper/googlefit-empty.json"); + emptyNode = objectMapper.readTree(resource.getInputStream()); + + } + + @Test + public void asDataPointsShouldReturnCorrectNumberOfDataPoints() { + + List> dataPoints = mapper.asDataPoints(singletonList(responseNode)); + assertThat(dataPoints.size(), equalTo(3)); + } + + @Test + public void asDataPointsShouldReturnCorrectDataPoints() { + + List> dataPoints = mapper.asDataPoints(singletonList(responseNode)); + testGoogleFitDataPoint(dataPoints.get(0), + createIntegerTestProperties(4146, "2015-02-02T22:49:39.811Z", "2015-02-02T23:25:20.811Z", + "derived:com.google.step_count.delta:com.nike.plusgps:")); + testGoogleFitDataPoint(dataPoints.get(1), + createIntegerTestProperties(17, "2015-07-10T21:58:17.687316406Z", "2015-07-10T21:59:17.687316406Z", + "derived:com.google.step_count.cumulative:com.google.android.gms:samsung:Galaxy " + + "Nexus:32b1bd9e:soft_step_counter")); + } + + @Test + public void asDataPointsShouldReturnSelfReportedAsModalityWhenDataSourceContainsUserInput() { + + List> dataPoints = mapper.asDataPoints(singletonList(responseNode)); + + assertThat(dataPoints.get(2).getHeader().getAcquisitionProvenance().getModality(), + equalTo(DataPointModality.SELF_REPORTED)); + + assertThat(dataPoints.get(0).getHeader().getAcquisitionProvenance().getModality(), nullValue()); + + } + + @Test + public void asDataPointsShouldReturnZeroDataPointsWithEmptyData() { + + List> dataPoints = mapper.asDataPoints(singletonList(emptyNode)); + assertThat(dataPoints.size(), equalTo(0)); + } + + /* Helper methods */ + + @Override + public void testGoogleFitMeasureFromDataPoint(StepCount testMeasure, Map properties) { + + StepCount.Builder expectedStepCountBuilder = new StepCount.Builder((long) properties.get("intValue")); + setExpectedEffectiveTimeFrame(expectedStepCountBuilder, properties); + StepCount expectedStepCount = expectedStepCountBuilder.build(); + + assertThat(testMeasure, equalTo(expectedStepCount)); + } +} diff --git a/shim-server/src/test/java/org/openmhealth/shim/ihealth/IHealthShimTest.java.txt b/shim-server/src/test/java/org/openmhealth/shim/ihealth/IHealthShimTest.java.txt new file mode 100644 index 00000000..5001f2b1 --- /dev/null +++ b/shim-server/src/test/java/org/openmhealth/shim/ihealth/IHealthShimTest.java.txt @@ -0,0 +1,90 @@ +package org.openmhealth.shim.ihealth; + +import static org.junit.Assert.assertEquals; + +import java.util.List; + +import org.junit.Test; +import org.openmhealth.schema.pojos.Activity; +import org.openmhealth.schema.pojos.BloodGlucose; +import org.openmhealth.schema.pojos.BloodGlucoseUnitValue; +import org.openmhealth.schema.pojos.BloodPressure; +import org.openmhealth.schema.pojos.BodyWeight; +import org.openmhealth.schema.pojos.HeartRate; +import org.openmhealth.schema.pojos.HeartRateUnitValue; +import org.openmhealth.schema.pojos.SleepDuration; +import org.openmhealth.schema.pojos.StepCount; +import org.openmhealth.schema.pojos.generic.DurationUnitValue.DurationUnit; +import org.openmhealth.schema.pojos.generic.MassUnitValue.MassUnit; +import org.openmhealth.shim.testing.ShimTestSupport; + +public class IHealthShimTest extends ShimTestSupport { + + @Test + public void testActivity() { + + List datapoints = read("ihealth-activity.json", Activity.SCHEMA_ACTIVITY, IHealthShim.IHealthDataTypes.ACTIVITY.getNormalizer()); + assertEquals(1, datapoints.size()); + + assertTimeFrameEquals("2014-12-11T10:00:00Z", "2014-12-11T11:00:00Z", datapoints.get(0).getEffectiveTimeFrame()); + assertEquals("Run", datapoints.get(0).getActivityName()); + } + + @Test + public void testBloodGlucose() { + + List datapoints = read("ihealth-blood-glucose.json", BloodGlucose.SCHEMA_BLOOD_GLUCOSE, IHealthShim.IHealthDataTypes.BLOOD_GLUCOSE.getNormalizer()); + assertEquals(1, datapoints.size()); + + assertTimeFrameEquals("2014-12-11T12:30:49Z", datapoints.get(0).getEffectiveTimeFrame()); + assertBloodGlucoseUnitEquals("80.5", BloodGlucoseUnitValue.Unit.mg_dL, datapoints.get(0).getBloodGlucose()); + assertEquals("testing", datapoints.get(0).getNotes()); + } + + @Test + public void testBloodPressure() { + + List bloodPressure = read("ihealth-blood-pressure.json", BloodPressure.SCHEMA_BLOOD_PRESSURE, IHealthShim.IHealthDataTypes.BLOOD_PRESSURE.getNormalizer()); + assertEquals(1, bloodPressure.size()); + + assertTimeFrameEquals("2014-12-11T08:55:56Z", bloodPressure.get(0).getEffectiveTimeFrame()); + assertBloodPressureEquals(100, 50, bloodPressure.get(0)); + assertEquals("testing", bloodPressure.get(0).getNotes()); + + List heartRate = read("ihealth-blood-pressure.json", HeartRate.SCHEMA_HEART_RATE, IHealthShim.IHealthDataTypes.BLOOD_PRESSURE.getNormalizer()); + assertEquals(1, heartRate.size()); + + assertTimeFrameEquals("2014-12-11T08:55:56Z", heartRate.get(0).getEffectiveTimeFrame()); + assertHeartRateUnitEquals(60, HeartRateUnitValue.Unit.bpm, heartRate.get(0).getHeartRate()); + } + + @Test + public void testBodyWeight() { + + List datapoints = read("ihealth-body-weight.json", BodyWeight.SCHEMA_BODY_WEIGHT, IHealthShim.IHealthDataTypes.BODY_WEIGHT.getNormalizer()); + assertEquals(2, datapoints.size()); + + assertTimeFrameEquals("2014-12-10T07:38:10Z", datapoints.get(0).getEffectiveTimeFrame()); + assertMassUnitEquals("75.86718536514545", MassUnit.kg, datapoints.get(0).getMassUnitValue()); + } + + @Test + public void testSleep() { + + List datapoints = read("ihealth-sleep.json", SleepDuration.SCHEMA_SLEEP_DURATION, IHealthShim.IHealthDataTypes.SLEEP.getNormalizer()); + assertEquals(1, datapoints.size()); + + assertTimeFrameEquals("2014-12-10T23:00:00Z", "2014-12-11T07:00:00Z", datapoints.get(0).getEffectiveTimeFrame()); + assertEquals("zzz", datapoints.get(0).getNotes()); + } + + @Test + public void testStepCount() { + + List datapoints = read("ihealth-step-count.json", StepCount.SCHEMA_STEP_COUNT, IHealthShim.IHealthDataTypes.STEP_COUNT.getNormalizer()); + assertEquals(1, datapoints.size()); + + assertTimeFrameEquals("2014-12-11T07:00:00Z", 1, DurationUnit.d, datapoints.get(0).getEffectiveTimeFrame()); + assertEquals(Integer.valueOf(9000), datapoints.get(0).getStepCount()); + } +} diff --git a/shim-server/src/test/java/org/openmhealth/shim/jawbone/JawboneShimTest.java b/shim-server/src/test/java/org/openmhealth/shim/jawbone/JawboneShimTest.java deleted file mode 100644 index f227e287..00000000 --- a/shim-server/src/test/java/org/openmhealth/shim/jawbone/JawboneShimTest.java +++ /dev/null @@ -1,93 +0,0 @@ -/* - * Copyright 2014 Open mHealth - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.openmhealth.shim.jawbone; - - -import com.fasterxml.jackson.databind.ObjectMapper; -import com.fasterxml.jackson.databind.module.SimpleModule; -import org.joda.time.DateTime; -import org.joda.time.DateTimeZone; -import org.joda.time.format.DateTimeFormat; -import org.junit.Test; -import org.openmhealth.schema.pojos.StepCount; -import org.openmhealth.shim.ShimDataResponse; - -import java.io.IOException; -import java.io.InputStream; -import java.math.BigDecimal; -import java.net.URL; -import java.util.List; -import java.util.Map; - -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertNotNull; -import static org.junit.Assert.assertTrue; - -/** - * @author Danilo Bonilla - */ -public class JawboneShimTest { - - @Test - @SuppressWarnings("unchecked") - public void testParse() throws IOException { - URL url = Thread.currentThread().getContextClassLoader().getResource("jawbone-moves.json"); - assert url != null; - InputStream inputStream = url.openStream(); - - ObjectMapper objectMapper = new ObjectMapper(); - - JawboneShim.JawboneDataTypes.MOVES.getNormalizer(); - SimpleModule module = new SimpleModule(); - module.addDeserializer(ShimDataResponse.class, - JawboneShim.JawboneDataTypes.MOVES.getNormalizer()); - - objectMapper.registerModule(module); - - ShimDataResponse response = - objectMapper.readValue(inputStream, ShimDataResponse.class); - - assertNotNull(response); - assertNotNull(response.getShim()); - - Map map = (Map) response.getBody(); - assertTrue(map.containsKey(StepCount.SCHEMA_STEP_COUNT)); - - List stepCounts = (List) map.get(StepCount.SCHEMA_STEP_COUNT); - assertTrue(stepCounts != null && stepCounts.size() == 4); - - final String EXPECTED_HOURLY_TOTAL_TIMESTAMP = "2013112101"; - - DateTime expectedTimeUTC = - DateTimeFormat.forPattern("yyyyMMddHH").withZone(DateTimeZone.forID("America/Los_Angeles")) - .parseDateTime(EXPECTED_HOURLY_TOTAL_TIMESTAMP); - expectedTimeUTC = expectedTimeUTC.toDateTime(DateTimeZone.UTC); - - BigDecimal expectedDuration = new BigDecimal(793); - Integer expectedSteps = 1603; - - Boolean found = false; - for (StepCount sc : stepCounts) { - if (sc.getEffectiveTimeFrame().getTimeInterval().getStartTime().equals(expectedTimeUTC)) { - assertEquals(sc.getStepCount(), expectedSteps); - assertEquals(sc.getEffectiveTimeFrame().getTimeInterval().getDuration().getValue(), expectedDuration); - found = true; - } - } - assertTrue(found); - } -} diff --git a/shim-server/src/test/java/org/openmhealth/shim/jawbone/mapper/JawboneBodyMassIndexDataPointMapperUnitTests.java b/shim-server/src/test/java/org/openmhealth/shim/jawbone/mapper/JawboneBodyMassIndexDataPointMapperUnitTests.java new file mode 100644 index 00000000..7f21c228 --- /dev/null +++ b/shim-server/src/test/java/org/openmhealth/shim/jawbone/mapper/JawboneBodyMassIndexDataPointMapperUnitTests.java @@ -0,0 +1,85 @@ +package org.openmhealth.shim.jawbone.mapper; + +import com.google.common.collect.Maps; +import org.openmhealth.schema.domain.omh.*; +import org.springframework.core.io.ClassPathResource; +import org.testng.annotations.BeforeTest; +import org.testng.annotations.Test; + +import java.io.IOException; +import java.time.OffsetDateTime; +import java.util.List; +import java.util.Map; + +import static java.util.Collections.singletonList; +import static org.hamcrest.CoreMatchers.equalTo; +import static org.hamcrest.MatcherAssert.assertThat; + + +/** + * @author Chris Schaefbauer + */ +public class JawboneBodyMassIndexDataPointMapperUnitTests extends JawboneDataPointMapperUnitTests { + + private JawboneBodyMassIndexDataPointMapper mapper = new JawboneBodyMassIndexDataPointMapper(); + + + @BeforeTest + public void initializeResponseNodes() throws IOException { + ClassPathResource resource = + new ClassPathResource("org/openmhealth/shim/jawbone/mapper/jawbone-body-events.json"); + responseNode = objectMapper.readTree(resource.getInputStream()); + initializeEmptyNode(); + } + + @Test + public void asDataPointsShouldReturnNoDataPointsWithEmptyResponse() { + + testEmptyNode(mapper); + } + + @Test + public void asDataPointsShouldReturnCorrectNumberOfDataPoints() { + + List> dataPoints = mapper.asDataPoints(singletonList(responseNode)); + assertThat(dataPoints.size(), equalTo(2)); + } + + // TODO why are first and last special? + @Test + public void asDataPointsShouldReturnCorrectFirstDataPoint() { + + List> dataPoints = mapper.asDataPoints(singletonList(responseNode)); + + BodyMassIndex expectedBodyMassIndex = new BodyMassIndex + .Builder(new TypedUnitValue<>(BodyMassIndexUnit.KILOGRAMS_PER_SQUARE_METER, 24)) + .setEffectiveTimeFrame(OffsetDateTime.parse("2015-08-11T22:37:18-06:00")) + .build(); + assertThat(dataPoints.get(0).getBody(), equalTo(expectedBodyMassIndex)); + + Map testProperties = Maps.newHashMap(); + testProperties.put(HEADER_SCHEMA_ID_KEY, BodyMassIndex.SCHEMA_ID); + testProperties.put(HEADER_EXTERNAL_ID_KEY, "QkfTizSpRdukQY3ns4PYbkucZTM5yPMg"); + testProperties.put(HEADER_SOURCE_UPDATE_KEY, "2015-08-13T08:23:58Z"); + testProperties.put(HEADER_SHARED_KEY, true); + testDataPointHeader(dataPoints.get(0).getHeader(), testProperties); + } + + @Test + public void asDataPointsShouldReturnCorrectLastDataPoint() { + + List> dataPoints = mapper.asDataPoints(singletonList(responseNode)); + BodyMassIndex expectedBodyMassIndex = new BodyMassIndex + .Builder(new TypedUnitValue<>(BodyMassIndexUnit.KILOGRAMS_PER_SQUARE_METER, 25.2)) + .setEffectiveTimeFrame(OffsetDateTime.parse("2015-08-06T23:36:57-06:00")) + .build(); + assertThat(dataPoints.get(1).getBody(), equalTo(expectedBodyMassIndex)); + + Map testProperties = Maps.newHashMap(); + testProperties.put(HEADER_EXTERNAL_ID_KEY, "QkfTizSpRdt6MGLRxULIlVTscmwD_cPJ"); + testProperties.put(HEADER_SCHEMA_ID_KEY, BodyMassIndex.SCHEMA_ID); + testProperties.put(HEADER_SOURCE_UPDATE_KEY, "2015-08-07T05:36:57Z"); + testProperties.put(HEADER_SHARED_KEY, false); + testDataPointHeader(dataPoints.get(1).getHeader(), testProperties); + } +} diff --git a/shim-server/src/test/java/org/openmhealth/shim/jawbone/mapper/JawboneBodyWeightDataPointMapperUnitTests.java b/shim-server/src/test/java/org/openmhealth/shim/jawbone/mapper/JawboneBodyWeightDataPointMapperUnitTests.java new file mode 100644 index 00000000..b5fd7de4 --- /dev/null +++ b/shim-server/src/test/java/org/openmhealth/shim/jawbone/mapper/JawboneBodyWeightDataPointMapperUnitTests.java @@ -0,0 +1,93 @@ +package org.openmhealth.shim.jawbone.mapper; + +import com.google.common.collect.Maps; +import org.openmhealth.schema.domain.omh.BodyWeight; +import org.openmhealth.schema.domain.omh.DataPoint; +import org.openmhealth.schema.domain.omh.MassUnit; +import org.openmhealth.schema.domain.omh.MassUnitValue; +import org.springframework.core.io.ClassPathResource; +import org.testng.annotations.BeforeTest; +import org.testng.annotations.Test; + +import java.io.IOException; +import java.time.OffsetDateTime; +import java.util.List; +import java.util.Map; + +import static java.util.Collections.singletonList; +import static org.hamcrest.CoreMatchers.equalTo; +import static org.hamcrest.MatcherAssert.assertThat; + + +/** + * @author Chris Schaefbauer + */ +public class JawboneBodyWeightDataPointMapperUnitTests extends JawboneDataPointMapperUnitTests { + + JawboneBodyWeightDataPointMapper mapper = new JawboneBodyWeightDataPointMapper(); + + @BeforeTest + public void initializeResponseNodes() throws IOException { + + ClassPathResource resource = + new ClassPathResource("org/openmhealth/shim/jawbone/mapper/jawbone-body-events.json"); + responseNode = objectMapper.readTree(resource.getInputStream()); + initializeEmptyNode(); + } + + @Test + public void asDataPointsShouldReturnNoDataPointsWithEmptyResponse() { + + testEmptyNode(mapper); + } + + @Test + public void asDataPointsShouldReturnCorrectNumberOfDataPoints() { + + List> dataPoints = mapper.asDataPoints(singletonList(responseNode)); + assertThat(dataPoints.size(), equalTo(2)); + } + + @Test + public void asDataPointsShouldReturnCorrectDataPointWhenUserNotePropertyIsInResponse() { + + List> dataPoints = mapper.asDataPoints(singletonList(responseNode)); + + BodyWeight testBodyWeight = dataPoints.get(0).getBody(); + BodyWeight expectedBodyWeight = new BodyWeight. + Builder(new MassUnitValue(MassUnit.KILOGRAM, 86.0010436535)). + setEffectiveTimeFrame(OffsetDateTime.parse("2015-08-11T22:37:20-06:00")). + setUserNotes("First weight"). + build(); + + assertThat(testBodyWeight, equalTo(expectedBodyWeight)); + + Map testProperties = Maps.newHashMap(); + testProperties.put("schemaId", BodyWeight.SCHEMA_ID); + testProperties.put("externalId", "QkfTizSpRdubVXHaj3iZk6Vd3qqdl_RJ"); + testProperties.put("sourceUpdatedDateTime", "2015-08-12T04:37:20Z"); + testProperties.put("shared", false); + + testDataPointHeader(dataPoints.get(0).getHeader(), testProperties); + } + + @Test + public void asDataPointsShouldReturnCorrectDataPointWhenUserNoteIsNull() { + + List> dataPoints = mapper.asDataPoints(singletonList(responseNode)); + BodyWeight testBodyWeight = dataPoints.get(1).getBody(); + BodyWeight expectedBodyWeight = new BodyWeight.Builder(new MassUnitValue(MassUnit.KILOGRAM, 86.5010436535)) + .setEffectiveTimeFrame(OffsetDateTime.parse("2015-08-11T22:37:18-06:00")) + .build(); + assertThat(testBodyWeight, equalTo(expectedBodyWeight)); + + Map testProperties = Maps.newHashMap(); + testProperties.put("schemaId", BodyWeight.SCHEMA_ID); + testProperties.put("externalId", "QkfTizSpRdukQY3ns4PYbkucZTM5yPMg"); + testProperties.put("sourceUpdatedDateTime", "2015-08-13T08:23:58Z"); + testProperties.put("shared", true); + testDataPointHeader(dataPoints.get(1).getHeader(), testProperties); + } + + +} diff --git a/shim-server/src/test/java/org/openmhealth/shim/jawbone/mapper/JawboneDataPointMapperUnitTests.java b/shim-server/src/test/java/org/openmhealth/shim/jawbone/mapper/JawboneDataPointMapperUnitTests.java new file mode 100644 index 00000000..8201cd87 --- /dev/null +++ b/shim-server/src/test/java/org/openmhealth/shim/jawbone/mapper/JawboneDataPointMapperUnitTests.java @@ -0,0 +1,220 @@ +package org.openmhealth.shim.jawbone.mapper; + +import com.fasterxml.jackson.databind.JsonNode; +import org.openmhealth.schema.domain.omh.*; +import org.openmhealth.shim.common.mapper.DataPointMapperUnitTests; +import org.springframework.core.io.ClassPathResource; +import org.testng.annotations.Test; + +import java.io.IOException; +import java.time.OffsetDateTime; +import java.util.List; +import java.util.Map; +import java.util.Optional; + +import static java.util.Collections.singletonList; +import static org.hamcrest.CoreMatchers.equalTo; +import static org.hamcrest.CoreMatchers.nullValue; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.openmhealth.shim.jawbone.mapper.JawboneDataPointMapper.RESOURCE_API_SOURCE_NAME; + + +// TODO clean up +/** + * @author Chris Schaefbauer + */ +public abstract class JawboneDataPointMapperUnitTests extends DataPointMapperUnitTests { + + static final String HEADER_EXTERNAL_ID_KEY = "externalId"; + static final String HEADER_SCHEMA_ID_KEY = "schemaId"; + static final String HEADER_SOURCE_UPDATE_KEY = "sourceUpdatedDateTime"; + static final String HEADER_SHARED_KEY = "shared"; + static final String HEADER_SENSED_KEY = "sensed"; + + JawboneDataPointMapper sensedMapper = new JawboneDataPointMapper() { + @Override + protected Optional getMeasure(JsonNode listEntryNode) { + return null; + } + + @Override + protected boolean isSensed(JsonNode listEntryNode) { + return true; + } + }; + + JawboneDataPointMapper unsensedMapper = new JawboneDataPointMapper() { + @Override + protected Optional getMeasure(JsonNode listEntryNode) { + return null; + } + + @Override + protected boolean isSensed(JsonNode listEntryNode) { + return false; + } + }; + + JsonNode responseNode; + JsonNode emptyNode; + + public abstract void initializeResponseNodes() throws IOException; + + public void initializeEmptyNode() throws IOException { + ClassPathResource resource = + new ClassPathResource("org/openmhealth/shim/jawbone/mapper/jawbone-empty.json"); + emptyNode = objectMapper.readTree(resource.getInputStream()); + } + + /* Tests */ + + @Test + public void setEffectiveTimeFrameShouldSetCorrectForDateTime() throws IOException { + + JsonNode testDateTimeNode = objectMapper.readTree("{\n" + + "\"details\": {\n" + + "\"tz\": \"GMT-0600\"\n" + + "},\n" + + "\"time_created\": 1438747200,\n" + + "\"time_updated\": 1439867504\n" + + "}"); + + StepCount.Builder testBuilder = new StepCount.Builder(10); + sensedMapper.setEffectiveTimeFrame(testBuilder, testDateTimeNode); + StepCount stepCount = testBuilder.build(); + + assertThat(stepCount.getEffectiveTimeFrame().getDateTime(), + equalTo(OffsetDateTime.parse("2015-08-04T22:00:00-06:00"))); + } + + @Test + public void setEffectiveTimeFrameShouldSetCorrectTimeInterval() throws IOException { + + JsonNode testDateTimeNode = objectMapper.readTree("{\n" + + "\"details\": {\n" + + "\"tz\": \"GMT-0200\"\n" + + "},\n" + + "\"time_created\": 1439990403,\n" + + "\"time_updated\": 1439867504,\n" + + "\"time_completed\": 1439994003\n" + + "}"); + + StepCount.Builder testBuilder = new StepCount.Builder(10); + sensedMapper.setEffectiveTimeFrame(testBuilder, testDateTimeNode); + StepCount stepCount = testBuilder.build(); + + assertThat(stepCount.getEffectiveTimeFrame().getTimeInterval(), equalTo( + TimeInterval.ofStartDateTimeAndEndDateTime(OffsetDateTime.parse("2015-08-19T11:20:03-02:00"), + OffsetDateTime.parse("2015-08-19T12:20:03-02:00")))); + } + + /* Header tests */ + @Test + public void getHeaderShouldReturnSensedInHeaderWhenIsSensedIsTrue() throws IOException { + + JsonNode headerInfoNode = objectMapper.readTree("{}"); + DataPointHeader header = sensedMapper.getHeader(headerInfoNode, (T) new StepCount.Builder(10).build()); + assertThat(header.getAcquisitionProvenance().getModality(), equalTo(DataPointModality.SENSED)); + } + + @Test + public void getHeaderShouldReturnNullForSensedInHeaderWhenIsSensedIsFalse() throws IOException { + + JsonNode headerInfoNode = objectMapper.readTree("{}"); + DataPointHeader header = unsensedMapper.getHeader(headerInfoNode, (T) new StepCount.Builder(10).build()); + assertThat(header.getAcquisitionProvenance().getModality(), nullValue()); + } + + @Test + public void getHeaderShouldReturnTrueForSharedInHeaderWhenNodeIsShared() throws IOException { + + JsonNode sharedNode = objectMapper.readTree("{\n" + + "\"shared\": true\n" + + "}"); + DataPointHeader sharedHeader = sensedMapper.getHeader(sharedNode, (T) new StepCount.Builder(10).build()); + assertThat(sharedHeader.getAdditionalProperty("shared").get(), equalTo(true)); + } + + @Test + public void getHeaderShouldReturnFalseForSharedInHeaderWhenNodeIsNotShared() throws IOException { + + JsonNode sharedNode = objectMapper.readTree("{\n" + + "\"shared\": false\n" + + "}"); + DataPointHeader sharedHeader = sensedMapper.getHeader(sharedNode, (T) new StepCount.Builder(10).build()); + assertThat(sharedHeader.getAdditionalProperty("shared").get(), equalTo(false)); + } + + @Test + public void getHeaderShouldReturnNullForSharedInHeaderWhenSharedPropertyDoesNotExist() throws IOException { + + JsonNode sharedNode = objectMapper.readTree("{}"); + DataPointHeader sharedHeader = sensedMapper.getHeader(sharedNode, (T) new StepCount.Builder(10).build()); + assertThat(sharedHeader.getAdditionalProperties().get("shared"), nullValue()); + } + + @Test + public void getHeaderShouldReturnExternalIdInHeaderWhenXidPropertyExists() throws IOException { + + JsonNode xidNode = objectMapper.readTree("{\n" + + "\"xid\": \"40F7_htRRnT8Vo7nRBZO1X\"\n" + + "}"); + DataPointHeader headerWithXid = sensedMapper.getHeader(xidNode, (T) new StepCount.Builder(10).build()); + assertThat(headerWithXid.getAcquisitionProvenance().getAdditionalProperty("external_id").get(), equalTo( + "40F7_htRRnT8Vo7nRBZO1X")); + } + + @Test + public void getHeaderShouldReturnNullForExternalIdInHeaderWhenXidPropertyDoesNotExist() throws IOException { + + JsonNode xidNode = objectMapper.readTree("{}"); + DataPointHeader headerWithXid = sensedMapper.getHeader(xidNode, (T) new StepCount.Builder(10).build()); + assertThat(headerWithXid.getAcquisitionProvenance().getAdditionalProperties().get("external_id"), nullValue()); + } + + @Test + public void getHeaderShouldReturnCorrectUpdatedTimestampWhenUpdatedPropertyExists() throws IOException { + + JsonNode updatedNode = objectMapper.readTree("{\n" + + "\"time_updated\": 1439354240\n" + + "}"); + DataPointHeader headerWithTimeUpdated = + sensedMapper.getHeader(updatedNode, (T) new StepCount.Builder(10).build()); + assertThat(headerWithTimeUpdated.getAcquisitionProvenance().getAdditionalProperty("source_updated_date_time") + .get(), equalTo(OffsetDateTime.parse("2015-08-12T04:37:20Z"))); + } + + @Test + public void getHeaderShouldReturnNullWhenUpdatedPropertyDoesNotExist() throws IOException { + + JsonNode updatedNode = objectMapper.readTree("{}"); + DataPointHeader headerWithTimeUpdated = + sensedMapper.getHeader(updatedNode, (T) new StepCount.Builder(10).build()); + assertThat(headerWithTimeUpdated.getAcquisitionProvenance().getAdditionalProperties() + .get("source_updated_date_time"), nullValue()); + } + + + /* Test helper classes */ + + protected static void testDataPointHeader(DataPointHeader testMeasureHeader, Map testProperties) { + + assertThat(testMeasureHeader.getBodySchemaId(), equalTo(testProperties.get(HEADER_SCHEMA_ID_KEY))); + assertThat(testMeasureHeader.getAcquisitionProvenance().getSourceName(), equalTo(RESOURCE_API_SOURCE_NAME)); + assertThat(testMeasureHeader.getAcquisitionProvenance().getAdditionalProperties().get("external_id"), equalTo( + testProperties.getOrDefault(HEADER_EXTERNAL_ID_KEY, null))); + assertThat(testMeasureHeader.getAcquisitionProvenance().getAdditionalProperties().get( + "source_updated_date_time"), equalTo(OffsetDateTime.parse((String) testProperties.get( + HEADER_SOURCE_UPDATE_KEY)))); + assertThat(testMeasureHeader.getAdditionalProperties().get(HEADER_SHARED_KEY), + equalTo(testProperties.getOrDefault(HEADER_SHARED_KEY, null))); + assertThat(testMeasureHeader.getAcquisitionProvenance().getModality(), + equalTo(testProperties.getOrDefault(HEADER_SENSED_KEY, null))); + } + + protected void testEmptyNode(JawboneDataPointMapper mapper) { + + List> dataPoints = mapper.asDataPoints(singletonList(emptyNode)); + assertThat(dataPoints.size(), equalTo(0)); + } +} diff --git a/shim-server/src/test/java/org/openmhealth/shim/jawbone/mapper/JawboneGetTimeZoneUnitTests.java b/shim-server/src/test/java/org/openmhealth/shim/jawbone/mapper/JawboneGetTimeZoneUnitTests.java new file mode 100644 index 00000000..6269bb5e --- /dev/null +++ b/shim-server/src/test/java/org/openmhealth/shim/jawbone/mapper/JawboneGetTimeZoneUnitTests.java @@ -0,0 +1,276 @@ +package org.openmhealth.shim.jawbone.mapper; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import org.testng.annotations.Test; + +import java.io.IOException; +import java.time.*; + +import static org.hamcrest.CoreMatchers.equalTo; +import static org.hamcrest.MatcherAssert.assertThat; + + +// TODO rename. what's "getTimeZone"? +/** + * @author Chris Schaefbauer + */ +public class JawboneGetTimeZoneUnitTests { + + ObjectMapper objectMapper = new ObjectMapper(); + + /* Test JawboneDataPointMapper.parseZone */ + + @Test + public void parseZoneShouldReturnCorrectOlsonTimeZoneId() throws IOException { + + JsonNode testOlsonTimeZoneNode = objectMapper.readTree("\"America/New_York\""); + + ZoneId testZoneId = JawboneDataPointMapper.parseZone(testOlsonTimeZoneNode); + ZoneId expectedZoneId = ZoneId.of("America/New_York"); + assertThat(testZoneId, equalTo(expectedZoneId)); + + OffsetDateTime testOffsetDateTime = OffsetDateTime.ofInstant(Instant.ofEpochSecond(1438747200), testZoneId); + OffsetDateTime expectedOffsetDateTime = OffsetDateTime.parse("2015-08-05T00:00:00-04:00"); + assertThat(testOffsetDateTime, equalTo(expectedOffsetDateTime)); + + } + + @Test + public void parseZoneShouldReturnCorrectSecondsOffsetTimeZoneId() throws IOException { + + JsonNode testSecondOffsetTimeZoneNode = objectMapper.readTree("-21600"); + + ZoneId testZoneId = JawboneDataPointMapper.parseZone(testSecondOffsetTimeZoneNode); + ZoneId expectedZoneId = ZoneId.of("-06:00"); + assertThat(testZoneId.getRules(), equalTo(expectedZoneId.getRules())); + + OffsetDateTime testOffsetDateTime = OffsetDateTime.ofInstant(Instant.ofEpochSecond(1438747200), testZoneId); + OffsetDateTime expectedOffsetDateTime = OffsetDateTime.parse("2015-08-04T22:00:00-06:00"); + assertThat(testOffsetDateTime, equalTo(expectedOffsetDateTime)); + + // Testing fractional for seconds offset + testSecondOffsetTimeZoneNode = objectMapper.readTree("12600"); + testZoneId = JawboneDataPointMapper.parseZone(testSecondOffsetTimeZoneNode); + expectedZoneId = ZoneId.of("+03:30"); + assertThat(testZoneId.getRules(), equalTo(expectedZoneId.getRules())); + + } + + @Test + public void parseZoneShouldReturnCorrectGmtOffsetTimeZoneID() throws IOException { + + JsonNode testGmtOffsetTimeZoneNode = objectMapper.readTree("\"GMT-0600\""); + + ZoneId testZoneId = JawboneDataPointMapper.parseZone(testGmtOffsetTimeZoneNode); + ZoneId expectedZoneId = ZoneId.of("-06:00"); + assertThat(testZoneId.getRules(), equalTo(expectedZoneId.getRules())); + + OffsetDateTime testOffsetDateTime = OffsetDateTime.ofInstant(Instant.ofEpochSecond(1438747200), testZoneId); + OffsetDateTime expectedOffsetDateTime = OffsetDateTime.parse("2015-08-04T22:00:00-06:00"); + assertThat(testOffsetDateTime, equalTo(expectedOffsetDateTime)); + } + + @Test + public void parseZoneShouldReturnCorrectTimeZoneForFractionalOffsetTimeZones() throws IOException { + + JsonNode fractionalTimeZoneWithName = objectMapper.readTree("\"Asia/Kathmandu\""); + + ZoneId testZoneId = JawboneDataPointMapper.parseZone(fractionalTimeZoneWithName); + ZoneId expectedZoneId = ZoneId.of("+05:45"); + assertThat(testZoneId.getRules().toString(), equalTo(expectedZoneId.getRules() + .toString())); //comparing toString because parsing fractional stores a bit more information so the + // rules don't appear identical + + OffsetDateTime testOffsetDateTime = OffsetDateTime.ofInstant(Instant.ofEpochSecond(1438747200), testZoneId); + OffsetDateTime expectedOffsetDateTime = OffsetDateTime.parse("2015-08-05T09:45:00+05:45"); + assertThat(testOffsetDateTime, equalTo(expectedOffsetDateTime)); + + JsonNode fractionalTimeZoneWithOffset = objectMapper.readTree("\"GMT+0330\""); + testZoneId = JawboneDataPointMapper.parseZone(fractionalTimeZoneWithOffset); + expectedZoneId = ZoneId.of("+03:30"); + assertThat(testZoneId.getRules(), equalTo(expectedZoneId.getRules())); + + testOffsetDateTime = OffsetDateTime.ofInstant(Instant.ofEpochSecond(1438747200), testZoneId); + expectedOffsetDateTime = OffsetDateTime.parse("2015-08-05T07:30:00+03:30"); + assertThat(testOffsetDateTime, equalTo(expectedOffsetDateTime)); + + } + + /* Test JawboneDataPointMapper.getTimeZoneForTimestamp */ + + @Test + public void getTimeZoneForTimestampShouldReturnCorrectTimeZoneForResponseWithoutTimeZoneList() throws IOException { + + JsonNode testDateTimeNode = objectMapper.readTree("{\n" + + "\"details\": {\n" + + "\"tz\": \"GMT-0200\"\n" + + "},\n" + + "\"time_created\": 1439990403,\n" + + "\"time_updated\": 1439867504,\n" + + "\"time_completed\": 1439994003\n" + + "}"); + + ZoneId timeZoneForTimestamp = JawboneDataPointMapper.getTimeZoneForTimestamp(testDateTimeNode, 1439990403L); + assertThat(timeZoneForTimestamp.getRules(), equalTo(ZoneId.of("-02:00").getRules())); + } + + @Test + public void getTimeZoneForTimestampShouldReturnZTimeZoneWhenTimeZoneIsMissing() throws IOException { + + JsonNode testDateTimeNode = objectMapper.readTree("{\n" + + "\"details\": {\n" + + "},\n" + + "\"time_created\": 1439990403,\n" + + "\"time_updated\": 1439867504,\n" + + "\"time_completed\": 1439994003\n" + + "}"); + + ZoneId timeZoneForTimestamp = JawboneDataPointMapper.getTimeZoneForTimestamp(testDateTimeNode, 1439990403L); + assertThat(timeZoneForTimestamp.getRules(), equalTo(ZoneId.of("Z").getRules())); + } + + @Test + public void getTimeZoneForTimestampShouldReturnZTimeZoneWhenTimeZoneIsNull() throws IOException { + + JsonNode testDateTimeNode = objectMapper.readTree("{\n" + + "\"details\": {\n" + + "\"tz\": null\n" + + "},\n" + + "\"time_created\": 1439990403,\n" + + "\"time_updated\": 1439867504,\n" + + "\"time_completed\": 1439994003\n" + + "}"); + + ZoneId timeZoneForTimestamp = JawboneDataPointMapper.getTimeZoneForTimestamp(testDateTimeNode, 1439990403L); + assertThat(timeZoneForTimestamp.getRules(), equalTo(ZoneId.of("Z").getRules())); + } + + @Test + public void getTimeZoneForTimestampShouldReturnCorrectTimeZonesForSingleTimeZoneInList() throws IOException { + + JsonNode testDateTimeNode = objectMapper.readTree("{\n" + + "\"details\": {\n" + + "\"tz\": null,\n" + + "\"tzs\": [\n" + + "[\n" + + "1439219760,\n" + + "\"America/Denver\"\n" + + "]\n" + + "]" + + "},\n" + + "\"time_created\": 1439990403,\n" + + "\"time_updated\": 1439867504,\n" + + "\"time_completed\": 1439994003\n" + + "}"); + + ZoneId timeZoneForTimestamp = JawboneDataPointMapper.getTimeZoneForTimestamp(testDateTimeNode, 1439990403L); + assertThat(timeZoneForTimestamp.getRules(), equalTo(ZoneId.of("America/Denver").getRules())); + + OffsetDateTime testOffsetDateTime = OffsetDateTime.of(2015, 07, 25, 8, 20, 00, 00, + timeZoneForTimestamp.getRules().getOffset(LocalDateTime.of(2015, 07, 25, 8, 20))); + assertThat(testOffsetDateTime, equalTo(OffsetDateTime.parse("2015-07-25T08:20:00-06:00"))); + + + } + + @Test + public void getTimeZoneForTimestampShouldReturnCorrectTimeZonesForMultipleTimeZones() throws IOException { + + JsonNode testDateTimeNode = objectMapper.readTree("{\n" + + "\"details\": {\n" + + "\"tz\": null,\n" + + "\"tzs\": [\n" + + "[\n" + + "1439219760,\n" + + "\"America/Denver\"\n" + + "],\n" + + "[\n" + + "1439494003,\n" + + "\"America/Los_Angeles\"\n" + + "],\n" + + "[\n" + + "1439994003,\n" + + "\"Pacific/Honolulu\"\n" + + "]\n" + + "]" + + "},\n" + + "\"time_created\": 1439990403,\n" + + "\"time_updated\": 1439867504,\n" + + "\"time_completed\": 1439994003\n" + + "}"); + + // Testing early time after first time zone, but before second + ZoneId timeZoneForTimestamp = JawboneDataPointMapper.getTimeZoneForTimestamp(testDateTimeNode, 1439221760L); + assertThat(timeZoneForTimestamp.getRules(), equalTo(ZoneId.of("America/Denver").getRules())); + + OffsetDateTime testOffsetDateTime = OffsetDateTime.of(2015, 07, 25, 8, 20, 00, 00, + timeZoneForTimestamp.getRules().getOffset(LocalDateTime.of(2015, 07, 25, 8, 20))); + assertThat(testOffsetDateTime, equalTo(OffsetDateTime.parse("2015-07-25T08:20:00-06:00"))); + + //Testing time equal to last timezone change + timeZoneForTimestamp = JawboneDataPointMapper.getTimeZoneForTimestamp(testDateTimeNode, 1439994003L); + assertThat(timeZoneForTimestamp.getRules(), equalTo(ZoneId.of("Pacific/Honolulu").getRules())); + + testOffsetDateTime = OffsetDateTime.of(2015, 07, 25, 8, 20, 00, 00, + timeZoneForTimestamp.getRules().getOffset(LocalDateTime.of(2015, 07, 25, 8, 20))); + assertThat(testOffsetDateTime, equalTo(OffsetDateTime.parse("2015-07-25T08:20:00-10:00"))); + + //Testing time after last timezone change + timeZoneForTimestamp = JawboneDataPointMapper.getTimeZoneForTimestamp(testDateTimeNode, 1440004003L); + assertThat(timeZoneForTimestamp.getRules(), equalTo(ZoneId.of("Pacific/Honolulu").getRules())); + + testOffsetDateTime = OffsetDateTime.of(2015, 07, 25, 8, 20, 00, 00, + timeZoneForTimestamp.getRules().getOffset(LocalDateTime.of(2015, 07, 25, 8, 20))); + assertThat(testOffsetDateTime, equalTo(OffsetDateTime.parse("2015-07-25T08:20:00-10:00"))); + + //Testing time between second and last timezone + timeZoneForTimestamp = JawboneDataPointMapper.getTimeZoneForTimestamp(testDateTimeNode, 1439894003L); + assertThat(timeZoneForTimestamp.getRules(), equalTo(ZoneId.of("America/Los_Angeles").getRules())); + + testOffsetDateTime = OffsetDateTime.of(2015, 07, 25, 8, 20, 00, 00, + timeZoneForTimestamp.getRules().getOffset(LocalDateTime.of(2015, 07, 25, 8, 20))); + assertThat(testOffsetDateTime, equalTo(OffsetDateTime.parse("2015-07-25T08:20:00-07:00"))); + + } + + @Test + public void getTimeZoneForTimestampShouldReturnCorrectTimeZoneBehaviorForDaylightSavings() throws IOException { + + JsonNode testDateTimeNode = objectMapper.readTree("{\n" + + "\"details\": {\n" + + "\"tz\": null,\n" + + "\"tzs\": [\n" + + "[\n" + + "1425796620,\n" + + "\"America/Denver\"\n" + + "]\n" + + "]" + + "},\n" + + "\"time_created\": 1425796620,\n" + + "\"time_updated\": 1439867504,\n" + + "\"time_completed\": 1425845420\n" + + "}"); + + ZoneId timeZoneForTimestamp = JawboneDataPointMapper.getTimeZoneForTimestamp(testDateTimeNode, 1425798620L); + assertThat(timeZoneForTimestamp.getRules(), equalTo(ZoneId.of("America/Denver").getRules())); + assertThat(timeZoneForTimestamp.getRules().getOffset(Instant.ofEpochSecond(1425798620)), equalTo(ZoneOffset.of( + "-07:00"))); + OffsetDateTime testOffsetDateTime = + OffsetDateTime.ofInstant(Instant.ofEpochSecond(1425796620), timeZoneForTimestamp); + OffsetDateTime expectedDateTime = OffsetDateTime.parse("2015-03-07T23:37:00-07:00"); + assertThat(testOffsetDateTime, equalTo(expectedDateTime)); + + timeZoneForTimestamp = JawboneDataPointMapper.getTimeZoneForTimestamp(testDateTimeNode, 1425845420L); + assertThat(timeZoneForTimestamp.getRules().getOffset(Instant.ofEpochSecond(1425845420)), + equalTo(ZoneOffset.of("-06:00"))); + + + testOffsetDateTime = + OffsetDateTime.ofInstant(Instant.ofEpochSecond(1425845420), timeZoneForTimestamp); + expectedDateTime = OffsetDateTime.parse("2015-03-08T14:10:20-06:00"); + assertThat(testOffsetDateTime, equalTo(expectedDateTime)); + + } + +} diff --git a/shim-server/src/test/java/org/openmhealth/shim/jawbone/mapper/JawboneHeartRateDataPointMapperUnitTests.java b/shim-server/src/test/java/org/openmhealth/shim/jawbone/mapper/JawboneHeartRateDataPointMapperUnitTests.java new file mode 100644 index 00000000..80e648bd --- /dev/null +++ b/shim-server/src/test/java/org/openmhealth/shim/jawbone/mapper/JawboneHeartRateDataPointMapperUnitTests.java @@ -0,0 +1,73 @@ +package org.openmhealth.shim.jawbone.mapper; + +import com.google.common.collect.Maps; +import org.openmhealth.schema.domain.omh.DataPoint; +import org.openmhealth.schema.domain.omh.DataPointModality; +import org.openmhealth.schema.domain.omh.HeartRate; +import org.openmhealth.schema.domain.omh.TemporalRelationshipToPhysicalActivity; +import org.springframework.core.io.ClassPathResource; +import org.testng.annotations.BeforeTest; +import org.testng.annotations.Test; + +import java.io.IOException; +import java.time.OffsetDateTime; +import java.util.List; +import java.util.Map; + +import static java.util.Collections.singletonList; +import static org.hamcrest.CoreMatchers.equalTo; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.openmhealth.schema.domain.omh.TemporalRelationshipToPhysicalActivity.AT_REST; + + +/** + * @author Chris Schaefbauer + */ +public class JawboneHeartRateDataPointMapperUnitTests extends JawboneDataPointMapperUnitTests { + + JawboneHeartRateDataPointMapper mapper = new JawboneHeartRateDataPointMapper(); + + @BeforeTest + public void initializeResponseNodes() throws IOException { + + ClassPathResource resource = + new ClassPathResource("org/openmhealth/shim/jawbone/mapper/jawbone-heartrates.json"); + responseNode = objectMapper.readTree(resource.getInputStream()); + initializeEmptyNode(); + } + + @Test + public void asDataPointsShouldReturnNoDataPointsWithEmptyResponse() { + + // FIXME this isn't intuitive, if this is a common test it should be pulled into the parent + testEmptyNode(mapper); + } + + @Test + public void asDataPointsShouldReturnCorrectNumberOfDataPoints() { + + List> dataPoints = mapper.asDataPoints(singletonList(responseNode)); + assertThat(dataPoints.size(), equalTo(1)); + } + + @Test + public void asDataPointsShouldReturnCorrectDataPoints() { + + List> dataPoints = mapper.asDataPoints(singletonList(responseNode)); + + HeartRate expectedHeartRate = new HeartRate.Builder(55) + .setTemporalRelationshipToPhysicalActivity(AT_REST) + .setEffectiveTimeFrame(OffsetDateTime.parse("2013-11-20T08:05:00-08:00")) + .build(); + + assertThat(dataPoints.get(0).getBody(), equalTo(expectedHeartRate)); + + // TODO kill maps + Map testProperties = Maps.newHashMap(); + testProperties.put(HEADER_EXTERNAL_ID_KEY, "40F7_htRRnT8Vo7nRBZO1X"); + testProperties.put(HEADER_SOURCE_UPDATE_KEY, "2013-11-21T15:59:59Z"); + testProperties.put(HEADER_SCHEMA_ID_KEY, HeartRate.SCHEMA_ID); + testProperties.put(HEADER_SENSED_KEY, DataPointModality.SENSED); + testDataPointHeader(dataPoints.get(0).getHeader(), testProperties); + } +} diff --git a/shim-server/src/test/java/org/openmhealth/shim/jawbone/mapper/JawbonePhysicalActivityDataPointMapperUnitTests.java b/shim-server/src/test/java/org/openmhealth/shim/jawbone/mapper/JawbonePhysicalActivityDataPointMapperUnitTests.java new file mode 100644 index 00000000..81f3cce4 --- /dev/null +++ b/shim-server/src/test/java/org/openmhealth/shim/jawbone/mapper/JawbonePhysicalActivityDataPointMapperUnitTests.java @@ -0,0 +1,204 @@ +package org.openmhealth.shim.jawbone.mapper; + +import com.beust.jcommander.internal.Maps; +import com.fasterxml.jackson.databind.JsonNode; +import org.hamcrest.Matchers; +import org.openmhealth.schema.domain.omh.*; +import org.springframework.core.io.ClassPathResource; +import org.testng.annotations.BeforeTest; +import org.testng.annotations.Test; + +import java.io.IOException; +import java.time.OffsetDateTime; +import java.time.ZoneId; +import java.time.ZonedDateTime; +import java.util.List; +import java.util.Map; + +import static java.time.ZoneOffset.UTC; +import static java.util.Collections.singletonList; +import static org.hamcrest.CoreMatchers.equalTo; +import static org.hamcrest.CoreMatchers.notNullValue; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.greaterThan; +import static org.openmhealth.schema.domain.omh.DataPointModality.SENSED; +import static org.openmhealth.schema.domain.omh.DurationUnit.SECOND; +import static org.openmhealth.schema.domain.omh.LengthUnit.METER; +import static org.openmhealth.schema.domain.omh.PhysicalActivity.SelfReportedIntensity.MODERATE; +import static org.openmhealth.shim.jawbone.mapper.JawboneDataPointMapper.RESOURCE_API_SOURCE_NAME; +import static org.testng.Assert.assertFalse; +import static org.testng.Assert.assertTrue; + + +/** + * @author Emerson Farrugia + */ +public class JawbonePhysicalActivityDataPointMapperUnitTests extends JawboneDataPointMapperUnitTests { + + private final JawbonePhysicalActivityDataPointMapper mapper = new JawbonePhysicalActivityDataPointMapper(); + + private JsonNode responseNode; + + @BeforeTest + public void initializeResponseNodes() throws IOException { + + ClassPathResource resource = new ClassPathResource("org/openmhealth/shim/jawbone/mapper/jawbone-workouts.json"); + responseNode = objectMapper.readTree(resource.getInputStream()); + initializeEmptyNode(); + } + + @Test + public void asDataPointsShouldReturnNoDataPointsWithEmptyResponse() { + + testEmptyNode(mapper); + } + + @Test + public void asDataPointsShouldReturnCorrectNumberOfDataPoints() { + + List> dataPoints = mapper.asDataPoints(singletonList(responseNode)); + + assertThat(dataPoints, notNullValue()); + assertThat(dataPoints.size(), equalTo(3)); + } + + @Test + public void asDataPointsShouldReturnCorrectSensedDataPoints() { + + List> dataPoints = mapper.asDataPoints(singletonList(responseNode)); + + assertThat(dataPoints, notNullValue()); + assertThat(dataPoints.size(), greaterThan(0)); + + OffsetDateTime endDateTime = ZonedDateTime.of(2013, 11, 22, 5, 47, 0, 0, UTC) + .withZoneSameInstant(ZoneId.of("America/Los_Angeles")).toOffsetDateTime(); + TimeInterval effectiveTimeInterval = + TimeInterval.ofEndDateTimeAndDuration(endDateTime, new DurationUnitValue(SECOND, 2_460)); + + PhysicalActivity physicalActivity = new PhysicalActivity.Builder("Run") + .setDistance(new LengthUnitValue(METER, 5_116)) + .setEffectiveTimeFrame(effectiveTimeInterval) + .setReportedActivityIntensity(MODERATE) + .build(); + + DataPoint firstDataPoint = dataPoints.get(0); + + assertThat(firstDataPoint.getBody(), equalTo(physicalActivity)); + + DataPointAcquisitionProvenance acquisitionProvenance = firstDataPoint.getHeader().getAcquisitionProvenance(); + + assertThat(acquisitionProvenance, notNullValue()); + assertThat(acquisitionProvenance.getSourceName(), equalTo(RESOURCE_API_SOURCE_NAME)); + assertThat(acquisitionProvenance.getAdditionalProperty("external_id").isPresent(), equalTo(true)); + assertThat(acquisitionProvenance.getAdditionalProperty("external_id").get(), equalTo("40F7_htRRnT8Vo7nRBZO1X")); + assertThat(acquisitionProvenance.getModality(), notNullValue()); + assertThat(acquisitionProvenance.getModality(), equalTo(SENSED)); + } + + @Test + public void asDataPointsShouldReturnCorrectMissingSensedDataPoints() { + + List> dataPoints = mapper.asDataPoints(singletonList(responseNode)); + + PhysicalActivity expectedPhysicalActivity = new PhysicalActivity.Builder("Bike") + .setDistance(new LengthUnitValue(METER, 1188)) + .setEffectiveTimeFrame( + TimeInterval.ofEndDateTimeAndDuration(OffsetDateTime.parse("2015-04-29T16:07:07-04:00"), + new DurationUnitValue(SECOND, 343))) + .build(); + + assertThat(dataPoints.get(1).getBody(), equalTo(expectedPhysicalActivity)); + + DataPointHeader testDataPointHeader = dataPoints.get(1).getHeader(); + Map testProperties = Maps.newHashMap(); + testProperties.put(HEADER_EXTERNAL_ID_KEY, "SbiOBJjJJk8n2xLpNTMFng12pGRjX-qe"); + testProperties.put(HEADER_SOURCE_UPDATE_KEY, "2015-04-29T20:07:56Z"); + testProperties.put(HEADER_SCHEMA_ID_KEY, PhysicalActivity.SCHEMA_ID); + testDataPointHeader(testDataPointHeader, testProperties); + } + + // TODO multiple tests? + @Test + public void asSelfReportedIntensityShouldReturnCorrectIntensityWhenWithinSpecification() { + PhysicalActivity.SelfReportedIntensity selfReportedIntensity = mapper.asSelfReportedIntensity(1); + assertThat(selfReportedIntensity, Matchers.equalTo(PhysicalActivity.SelfReportedIntensity.LIGHT)); + + selfReportedIntensity = mapper.asSelfReportedIntensity(2); + assertThat(selfReportedIntensity, Matchers.equalTo(PhysicalActivity.SelfReportedIntensity.MODERATE)); + + selfReportedIntensity = mapper.asSelfReportedIntensity(3); + assertThat(selfReportedIntensity, Matchers.equalTo(PhysicalActivity.SelfReportedIntensity.MODERATE)); + + selfReportedIntensity = mapper.asSelfReportedIntensity(4); + assertThat(selfReportedIntensity, Matchers.equalTo(PhysicalActivity.SelfReportedIntensity.VIGOROUS)); + + selfReportedIntensity = mapper.asSelfReportedIntensity(5); + assertThat(selfReportedIntensity, Matchers.equalTo(PhysicalActivity.SelfReportedIntensity.VIGOROUS)); + + } + + @Test(expectedExceptions = IllegalArgumentException.class) + public void asSelfReportedIntensityShouldThrowExceptionWhenOutsideSpecification() { + + mapper.asSelfReportedIntensity(6); + } + + // TODO experiment with formatting and quote replacement + @Test + public void isSensedShouldReturnTrueWhenStepsArePresent() throws IOException { + + JsonNode nodeWithSteps = objectMapper.readTree("{\n" + + "\"details\": {\n" + + "\"steps\": 5128,\n" + + "\"time\": 2460\n" + + "}\n" + + "}"); + assertTrue(mapper.isSensed(nodeWithSteps)); + } + + @Test + public void isSensedShouldReturnFalseWhenStepsAreNotPresent() throws IOException { + + JsonNode nodeWithSteps = objectMapper.readTree("{\n" + + "\"details\": {\n" + + "\"steps\": 0,\n" + + "\"time\": 2460\n" + + "}\n" + + "}"); + assertFalse(mapper.isSensed(nodeWithSteps)); + + nodeWithSteps = objectMapper.readTree("{\n" + + "\"details\": {\n" + + "\"time\": 2460\n" + + "}\n" + + "}"); + assertFalse(mapper.isSensed(nodeWithSteps)); + } + + @Test + public void getActivityNameShouldReturnTitleWhenPresent() { + + String activityName = mapper.getActivityName("run", 15); + assertThat(activityName, equalTo("run")); + + } + + @Test + public void getActivityNameShouldReturnCorrectSubtypeWhenTitleIsMissing() { + + String activityName = mapper.getActivityName(null, 9); + assertThat(activityName, equalTo("crossfit")); + + activityName = mapper.getActivityName(null, 29); + assertThat(activityName, equalTo("workout")); + + } + + @Test + public void getActivityNameShouldReturnGenericWorkoutNameWhenSubtypeAndTitleAreMissing() { + + String activityName = mapper.getActivityName(null, null); + assertThat(activityName, equalTo("workout")); + } + +} \ No newline at end of file diff --git a/shim-server/src/test/java/org/openmhealth/shim/jawbone/mapper/JawboneSleepDurationDataPointMapperUnitTests.java b/shim-server/src/test/java/org/openmhealth/shim/jawbone/mapper/JawboneSleepDurationDataPointMapperUnitTests.java new file mode 100644 index 00000000..dce7363e --- /dev/null +++ b/shim-server/src/test/java/org/openmhealth/shim/jawbone/mapper/JawboneSleepDurationDataPointMapperUnitTests.java @@ -0,0 +1,171 @@ +package org.openmhealth.shim.jawbone.mapper; + +import com.fasterxml.jackson.databind.JsonNode; +import com.google.common.collect.Maps; +import org.openmhealth.schema.domain.omh.*; +import org.springframework.core.io.ClassPathResource; +import org.testng.annotations.BeforeTest; +import org.testng.annotations.Test; + +import java.io.IOException; +import java.time.OffsetDateTime; +import java.util.List; +import java.util.Map; + +import static java.util.Collections.singletonList; +import static org.hamcrest.CoreMatchers.equalTo; +import static org.hamcrest.MatcherAssert.assertThat; + + +/** + * @author Chris Schaefbauer + */ +public class JawboneSleepDurationDataPointMapperUnitTests extends JawboneDataPointMapperUnitTests { + + JawboneSleepDurationDataPointMapper mapper = new JawboneSleepDurationDataPointMapper(); + + @BeforeTest + public void initializeResponseNodes() throws IOException { + + ClassPathResource resource = + new ClassPathResource("org/openmhealth/shim/jawbone/mapper/jawbone-sleeps.json"); + responseNode = objectMapper.readTree(resource.getInputStream()); + initializeEmptyNode(); + } + + @Test + public void asDataPointsShouldReturnNoDataPointsWithEmptyResponse() { + + testEmptyNode(mapper); + } + + @Test + public void asDataPointsShouldReturnCorrectNumberOfDataPoints() { + + List> dataPoints = mapper.asDataPoints(singletonList(responseNode)); + assertThat(dataPoints.size(), equalTo(2)); + } + + @Test + public void asDataPointsShouldReturnCorrectDataPointsWhenWakeUpCountEqualZeroAndShared() { + + List> dataPoints = mapper.asDataPoints(singletonList(responseNode)); + + SleepDuration expectedSleepDuration = new SleepDuration + .Builder(new DurationUnitValue(DurationUnit.SECOND, 10356)) + .setEffectiveTimeFrame( + TimeInterval.ofStartDateTimeAndEndDateTime(OffsetDateTime.parse("2015-08-04T22:48:51-06:00"), + OffsetDateTime.parse("2015-08-05T01:58:35-06:00"))) + .build(); + + expectedSleepDuration.setAdditionalProperty("wakeup_count", 0); + + assertThat(dataPoints.get(0).getBody(), equalTo(expectedSleepDuration)); + + Map testProperties = Maps.newHashMap(); + + testProperties.put(HEADER_EXTERNAL_ID_KEY, "QkfTizSpRdsDKwErMhvMqG9VDhpfyDGd"); + testProperties.put(HEADER_SOURCE_UPDATE_KEY, "2015-08-05T09:52:00Z"); + testProperties.put(HEADER_SCHEMA_ID_KEY, SleepDuration.SCHEMA_ID); + testProperties.put(HEADER_SHARED_KEY, true); + testProperties.put(HEADER_SENSED_KEY, DataPointModality.SENSED); + + testDataPointHeader(dataPoints.get(0).getHeader(), testProperties); + } + + @Test + public void asDataPointsShouldReturnCorrectDataPointsWhenWakeUpCountGreaterThanZeroAndNotShared() { + + List> dataPoints = mapper.asDataPoints(singletonList(responseNode)); + + SleepDuration expectedSleepDuration = + new SleepDuration.Builder(new DurationUnitValue(DurationUnit.SECOND, 27900)).setEffectiveTimeFrame( + TimeInterval.ofStartDateTimeAndEndDateTime(OffsetDateTime.parse("2015-08-03T23:05:00-04:00"), + OffsetDateTime.parse("2015-08-04T07:15:00-04:00"))).build(); + expectedSleepDuration.setAdditionalProperty("wakeup_count", 2); + + assertThat(dataPoints.get(1).getBody(), equalTo(expectedSleepDuration)); + + Map testProperties = Maps.newHashMap(); + + testProperties.put(HEADER_SHARED_KEY, false); + testProperties.put(HEADER_EXTERNAL_ID_KEY, "QkfTizSpRdvIs6MMJbKP6ulqeYwu5c2v"); + testProperties.put(HEADER_SCHEMA_ID_KEY, SleepDuration.SCHEMA_ID); + testProperties.put(HEADER_SOURCE_UPDATE_KEY, "2015-08-04T12:10:56Z"); + testProperties.put(HEADER_SENSED_KEY, DataPointModality.SENSED); + + testDataPointHeader(dataPoints.get(1).getHeader(), testProperties); + } + + @Test + public void isSensedShouldReturnTrueWhenAwakePropertyExistsAndGreaterThanZero() throws IOException { + + JsonNode testNode = objectMapper.readTree("{\n" + + "\"details\": {\n" + + "\"awake\": 100,\n" + + "\"light\": 0\n" + + "},\n" + + "\"time_created\": 1439990403\n" + + "}"); + assertThat(mapper.isSensed(testNode), equalTo(true)); + } + + @Test + public void isSensedShouldReturnTrueWhenLightPropertyExistsAndGreaterThanZero() throws IOException { + + JsonNode testNode = objectMapper.readTree("{\n" + + "\"details\": {\n" + + "\"awake\": 0,\n" + + "\"light\": 100\n" + + "},\n" + + "\"time_created\": 1439990403\n" + + "}"); + assertThat(mapper.isSensed(testNode), equalTo(true)); + } + + @Test + public void isSensedShouldReturnTrueWhenLightAndAwakePropertiesExistsAndGreaterThanZero() throws IOException { + + JsonNode testNode = objectMapper.readTree("{\n" + + "\"details\": {\n" + + "\"awake\": 100,\n" + + "\"light\": 100\n" + + "},\n" + + "\"time_created\": 1439990403\n" + + "}"); + assertThat(mapper.isSensed(testNode), equalTo(true)); + } + + @Test + public void isSensedShouldReturnFalseWhenLightAndAwakePropertiesAreEqualToZero() throws IOException { + + JsonNode testNode = objectMapper.readTree("{\n" + + "\"details\": {\n" + + "\"awake\": 0,\n" + + "\"light\": 0\n" + + "},\n" + + "\"time_created\": 1439990403\n" + + "}"); + assertThat(mapper.isSensed(testNode), equalTo(false)); + } + + @Test + public void isSensedShouldReturnFalseWhenLightOrAwakePropertiesAreMissing() throws IOException { + + JsonNode testNode = objectMapper.readTree("{\n" + + "\"details\": {\n" + + "\"awake\": 0\n" + + "},\n" + + "\"time_created\": 1439990403\n" + + "}"); + assertThat(mapper.isSensed(testNode), equalTo(false)); + + testNode = objectMapper.readTree("{\n" + + "\"details\": {\n" + + "\"light\": 0\n" + + "},\n" + + "\"time_created\": 1439990403\n" + + "}"); + assertThat(mapper.isSensed(testNode), equalTo(false)); + } +} diff --git a/shim-server/src/test/java/org/openmhealth/shim/jawbone/mapper/JawboneStepCountDataPointMapperUnitTests.java b/shim-server/src/test/java/org/openmhealth/shim/jawbone/mapper/JawboneStepCountDataPointMapperUnitTests.java new file mode 100644 index 00000000..cf3f83f9 --- /dev/null +++ b/shim-server/src/test/java/org/openmhealth/shim/jawbone/mapper/JawboneStepCountDataPointMapperUnitTests.java @@ -0,0 +1,88 @@ +package org.openmhealth.shim.jawbone.mapper; + +import com.google.common.collect.Maps; +import org.openmhealth.schema.domain.omh.DataPoint; +import org.openmhealth.schema.domain.omh.DataPointModality; +import org.openmhealth.schema.domain.omh.StepCount; +import org.springframework.core.io.ClassPathResource; +import org.testng.annotations.BeforeTest; +import org.testng.annotations.Test; + +import java.io.IOException; +import java.time.OffsetDateTime; +import java.util.List; +import java.util.Map; + +import static java.util.Collections.singletonList; +import static org.hamcrest.CoreMatchers.equalTo; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.openmhealth.schema.domain.omh.TimeInterval.ofStartDateTimeAndEndDateTime; + + +/** + * @author Chris Schaefbauer + */ +public class JawboneStepCountDataPointMapperUnitTests extends JawboneDataPointMapperUnitTests { + + private JawboneStepCountDataPointMapper mapper = new JawboneStepCountDataPointMapper(); + + @BeforeTest + public void initializeResponseNodes() throws IOException { + + ClassPathResource resource = + new ClassPathResource("org/openmhealth/shim/jawbone/mapper/jawbone-moves.json"); + responseNode = objectMapper.readTree(resource.getInputStream()); + initializeEmptyNode(); + } + + @Test + public void asDataPointsShouldReturnNoDataPointsWithEmptyResponse() { + + testEmptyNode(mapper); + } + + @Test + public void asDataPointsShouldReturnCorrectNumberOfDataPoints() { + + List> dataPoints = mapper.asDataPoints(singletonList(responseNode)); + assertThat(dataPoints.size(), equalTo(2)); + } + + @Test + public void asDataPointsShouldReturnCorrectDataPointsForSingleTimeZone() { + + List> dataPoints = mapper.asDataPoints(singletonList(responseNode)); + + StepCount expectedStepCount = new StepCount.Builder(197) + .setEffectiveTimeFrame(ofStartDateTimeAndEndDateTime( + OffsetDateTime.parse("2015-08-10T09:16:00-06:00"), + OffsetDateTime.parse("2015-08-10T11:43:00-06:00"))) + .build(); + + assertThat(dataPoints.get(0).getBody(), equalTo(expectedStepCount)); + + Map testProperties = Maps.newHashMap(); + + testProperties.put(HEADER_SCHEMA_ID_KEY, StepCount.SCHEMA_ID); + testProperties.put(HEADER_SOURCE_UPDATE_KEY, "2015-08-18T03:11:44Z"); + testProperties.put(HEADER_SENSED_KEY, DataPointModality.SENSED); + testProperties.put(HEADER_EXTERNAL_ID_KEY, "QkfTizSpRdvMvnHFctzItGNZMT-1F5vw"); + + testDataPointHeader(dataPoints.get(0).getHeader(), testProperties); + + } + + @Test + public void asDataPointsShouldUseCorrectTimeZoneWhenMultipleTimeZonesOnSingleDay() { + + List> dataPoints = mapper.asDataPoints(singletonList(responseNode)); + + StepCount expectedStepCount = new StepCount.Builder(593) + .setEffectiveTimeFrame(ofStartDateTimeAndEndDateTime( + OffsetDateTime.parse("2015-08-05T00:00:00-04:00"), + OffsetDateTime.parse("2015-08-05T06:42:00-06:00"))) + .build(); + + assertThat(dataPoints.get(1).getBody(), equalTo(expectedStepCount)); + } +} diff --git a/shim-server/src/test/java/org/openmhealth/shim/misfit/mapper/MisfitPhysicalActivityDataPointMapperUnitTests.java b/shim-server/src/test/java/org/openmhealth/shim/misfit/mapper/MisfitPhysicalActivityDataPointMapperUnitTests.java new file mode 100644 index 00000000..9c237a27 --- /dev/null +++ b/shim-server/src/test/java/org/openmhealth/shim/misfit/mapper/MisfitPhysicalActivityDataPointMapperUnitTests.java @@ -0,0 +1,92 @@ +package org.openmhealth.shim.misfit.mapper; + +import com.fasterxml.jackson.databind.JsonNode; +import org.hamcrest.Matchers; +import org.openmhealth.schema.domain.omh.*; +import org.openmhealth.shim.common.mapper.DataPointMapperUnitTests; +import org.springframework.core.io.ClassPathResource; +import org.testng.annotations.BeforeTest; +import org.testng.annotations.Test; + +import java.io.IOException; +import java.time.OffsetDateTime; +import java.time.ZoneOffset; +import java.util.Collections; +import java.util.List; + +import static java.util.Collections.singletonList; +import static org.hamcrest.CoreMatchers.equalTo; +import static org.hamcrest.CoreMatchers.notNullValue; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.greaterThan; +import static org.openmhealth.schema.domain.omh.DurationUnit.SECOND; +import static org.openmhealth.schema.domain.omh.LengthUnit.MILE; +import static org.openmhealth.shim.misfit.mapper.MisfitDataPointMapper.RESOURCE_API_SOURCE_NAME; + + +/** + * @author Emerson Farrugia + */ +public class MisfitPhysicalActivityDataPointMapperUnitTests extends DataPointMapperUnitTests { + + private final MisfitPhysicalActivityDataPointMapper mapper = new MisfitPhysicalActivityDataPointMapper(); + + private JsonNode responseNode; + + @BeforeTest + public void initializeResponseNode() throws IOException { + + ClassPathResource resource = new ClassPathResource("org/openmhealth/shim/misfit/mapper/misfit-sessions.json"); + responseNode = objectMapper.readTree(resource.getInputStream()); + } + + @Test + public void asDataPointsShouldReturnCorrectNumberOfDataPoints() { + + List> dataPoints = mapper.asDataPoints(Collections.singletonList(responseNode)); + + assertThat(dataPoints, notNullValue()); + assertThat(dataPoints.size(), equalTo(3)); + } + + @Test + public void asDataPointsShouldReturnCorrectDataPoints() { + + List> dataPoints = mapper.asDataPoints(Collections.singletonList(responseNode)); + + assertThat(dataPoints, notNullValue()); + assertThat(dataPoints.size(), greaterThan(0)); + + TimeInterval effectiveTimeInterval = TimeInterval.ofStartDateTimeAndDuration( + OffsetDateTime.of(2015, 4, 13, 11, 46, 0, 0, ZoneOffset.ofHours(-7)), + new DurationUnitValue(SECOND, 1140.0)); + + PhysicalActivity physicalActivity = new PhysicalActivity.Builder("Walking") + .setDistance(new LengthUnitValue(MILE, 0.9371)) + .setEffectiveTimeFrame(effectiveTimeInterval) + .build(); + + DataPoint firstDataPoint = dataPoints.get(0); + + assertThat(firstDataPoint.getBody(), equalTo(physicalActivity)); + + DataPointAcquisitionProvenance acquisitionProvenance = firstDataPoint.getHeader().getAcquisitionProvenance(); + + assertThat(acquisitionProvenance, notNullValue()); + assertThat(acquisitionProvenance.getSourceName(), equalTo(RESOURCE_API_SOURCE_NAME)); + assertThat(acquisitionProvenance.getAdditionalProperty("external_id").isPresent(), equalTo(true)); + assertThat(acquisitionProvenance.getAdditionalProperty("external_id").get(), + equalTo("552eab896c59ae1f7300003e")); + } + + @Test + public void asDataPointsShouldReturnEmptyListIfEmptyResponse() throws IOException { + + JsonNode emptyNode = objectMapper.readTree("{\n" + + " \"sessions\": []\n" + + "}"); + List> dataPoints = mapper.asDataPoints(singletonList(emptyNode)); + + assertThat(dataPoints.size(), Matchers.equalTo(0)); + } +} \ No newline at end of file diff --git a/shim-server/src/test/java/org/openmhealth/shim/misfit/mapper/MisfitSleepDurationDataPointMapperUnitTests.java b/shim-server/src/test/java/org/openmhealth/shim/misfit/mapper/MisfitSleepDurationDataPointMapperUnitTests.java new file mode 100644 index 00000000..54bac8cc --- /dev/null +++ b/shim-server/src/test/java/org/openmhealth/shim/misfit/mapper/MisfitSleepDurationDataPointMapperUnitTests.java @@ -0,0 +1,153 @@ +package org.openmhealth.shim.misfit.mapper; + +import com.fasterxml.jackson.databind.JsonNode; +import org.hamcrest.Matchers; +import org.openmhealth.schema.domain.omh.*; +import org.openmhealth.shim.common.mapper.DataPointMapperUnitTests; +import org.openmhealth.shim.common.mapper.JsonNodeMappingException; +import org.springframework.core.io.ClassPathResource; +import org.testng.annotations.BeforeTest; +import org.testng.annotations.Test; + +import java.io.IOException; +import java.time.OffsetDateTime; +import java.time.ZoneOffset; +import java.util.List; + +import static java.util.Collections.singletonList; +import static org.hamcrest.CoreMatchers.equalTo; +import static org.hamcrest.CoreMatchers.notNullValue; +import static org.hamcrest.CoreMatchers.nullValue; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.empty; +import static org.hamcrest.Matchers.greaterThan; +import static org.openmhealth.schema.domain.omh.DataPointModality.SENSED; +import static org.openmhealth.schema.domain.omh.DurationUnit.SECOND; +import static org.openmhealth.shim.misfit.mapper.MisfitDataPointMapper.RESOURCE_API_SOURCE_NAME; + + +/** + * @author Emerson Farrugia + */ +public class MisfitSleepDurationDataPointMapperUnitTests extends DataPointMapperUnitTests { + + private final MisfitSleepDurationDataPointMapper mapper = new MisfitSleepDurationDataPointMapper(); + + private JsonNode responseNode; + + @BeforeTest + public void initializeResponseNode() throws IOException { + + ClassPathResource resource = + new ClassPathResource("org/openmhealth/shim/misfit/mapper/misfit-sleeps.json"); + responseNode = objectMapper.readTree(resource.getInputStream()); + } + + @Test(expectedExceptions = JsonNodeMappingException.class) + public void asDataPointsShouldThrowExceptionOnEmptySleepDetails() throws IOException { + + JsonNode node = objectMapper.readTree("{\n" + + " \"sleeps\": [\n" + + " {\n" + + " \"id\": \"54fa13a8440f705a7406845f\",\n" + + " \"autoDetected\": false,\n" + + " \"startTime\": \"2015-02-24T21:40:59-05:00\",\n" + + " \"duration\": 2580,\n" + + " \"sleepDetails\": []\n" + + " }\n" + + " ]\n" + + "}"); + + mapper.asDataPoints(singletonList(node)); + } + + @Test + public void asDataPointsShouldReturnEmptyListIfAwake() throws IOException { + + JsonNode node = objectMapper.readTree("{\n" + + " \"sleeps\": [\n" + + " {\n" + + " \"id\": \"54fa13a8440f705a7406845f\",\n" + + " \"autoDetected\": false,\n" + + " \"startTime\": \"2015-02-24T21:40:59-05:00\",\n" + + " \"duration\": 2580,\n" + + " \"sleepDetails\": [\n" + + " {\n" + + " \"datetime\": \"2015-02-24T21:40:59-05:00\",\n" + + " \"value\": 1\n" + + " }\n" + + " ]\n" + + " }\n" + + " ]\n" + + "}"); + + List> dataPoints = mapper.asDataPoints(singletonList(node)); + + assertThat(dataPoints, notNullValue()); + assertThat(dataPoints, empty()); + } + + @Test + public void asDataPointsShouldReturnCorrectNumberOfDataPoints() { + + List> dataPoints = mapper.asDataPoints(singletonList(responseNode)); + + assertThat(dataPoints, notNullValue()); + assertThat(dataPoints.size(), equalTo(2)); + } + + @Test + public void asDataPointsShouldReturnEmptyListIfEmptyResponse() throws IOException { + + JsonNode emptyNode = objectMapper.readTree("{\n" + + " \"sleeps\": []\n" + + "}"); + List> dataPoints = mapper.asDataPoints(singletonList(emptyNode)); + + assertThat(dataPoints.size(), Matchers.equalTo(0)); + } + + @Test + public void asDataPointsShouldReturnCorrectDataPoints() { + + List> dataPoints = mapper.asDataPoints(singletonList(responseNode)); + + assertThat(dataPoints, notNullValue()); + assertThat(dataPoints.size(), greaterThan(0)); + + // the end time is the addition of the start time and the total duration + TimeInterval effectiveTimeInterval = TimeInterval.ofStartDateTimeAndEndDateTime( + OffsetDateTime.of(2015, 2, 23, 21, 40, 59, 0, ZoneOffset.ofHours(-5)), + OffsetDateTime.of(2015, 2, 24, 0, 53, 59, 0, ZoneOffset.ofHours(-5))); + + // the sleep duration is the total duration minus the sum of the awake segment durations + SleepDuration sleepDuration = new SleepDuration.Builder(new DurationUnitValue(SECOND, 10140)) + .setEffectiveTimeFrame(effectiveTimeInterval) + .build(); + + DataPoint firstDataPoint = dataPoints.get(0); + + assertThat(firstDataPoint.getBody(), equalTo(sleepDuration)); + + DataPointAcquisitionProvenance acquisitionProvenance = firstDataPoint.getHeader().getAcquisitionProvenance(); + + assertThat(acquisitionProvenance, notNullValue()); + assertThat(acquisitionProvenance.getSourceName(), equalTo(RESOURCE_API_SOURCE_NAME)); + assertThat(acquisitionProvenance.getModality(), equalTo(SENSED)); + } + + @Test + public void asDataPointsShouldSetModalityAsSensedOnlyWhenAutodetectedIsTrue() throws IOException { + + ClassPathResource resource = + new ClassPathResource("org/openmhealth/shim/misfit/mapper/misfit-sleeps-detected-and-not.json"); + JsonNode responseNodeForSleepSensing = objectMapper.readTree(resource.getInputStream()); + + List> dataPoints = mapper.asDataPoints(singletonList(responseNodeForSleepSensing)); + + assertThat(dataPoints.get(0).getHeader().getAcquisitionProvenance().getModality(), equalTo(SENSED)); + assertThat(dataPoints.get(1).getHeader().getAcquisitionProvenance().getModality(),nullValue()); + assertThat(dataPoints.get(2).getHeader().getAcquisitionProvenance().getModality(),nullValue()); + + } +} \ No newline at end of file diff --git a/shim-server/src/test/java/org/openmhealth/shim/misfit/mapper/MisfitStepCountDataPointMapperUnitTests.java b/shim-server/src/test/java/org/openmhealth/shim/misfit/mapper/MisfitStepCountDataPointMapperUnitTests.java new file mode 100644 index 00000000..30852c95 --- /dev/null +++ b/shim-server/src/test/java/org/openmhealth/shim/misfit/mapper/MisfitStepCountDataPointMapperUnitTests.java @@ -0,0 +1,88 @@ +package org.openmhealth.shim.misfit.mapper; + +import com.fasterxml.jackson.databind.JsonNode; +import org.hamcrest.Matchers; +import org.openmhealth.schema.domain.omh.*; +import org.openmhealth.shim.common.mapper.DataPointMapperUnitTests; +import org.springframework.core.io.ClassPathResource; +import org.testng.annotations.BeforeTest; +import org.testng.annotations.Test; + +import java.io.IOException; +import java.time.OffsetDateTime; +import java.util.List; + +import static java.time.ZoneOffset.UTC; +import static java.util.Collections.singletonList; +import static org.hamcrest.CoreMatchers.equalTo; +import static org.hamcrest.CoreMatchers.notNullValue; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.greaterThan; +import static org.openmhealth.schema.domain.omh.DurationUnit.DAY; +import static org.openmhealth.shim.misfit.mapper.MisfitDataPointMapper.RESOURCE_API_SOURCE_NAME; + + +/** + * @author Emerson Farrugia + */ +public class MisfitStepCountDataPointMapperUnitTests extends DataPointMapperUnitTests { + + private final MisfitStepCountDataPointMapper mapper = new MisfitStepCountDataPointMapper(); + + private JsonNode responseNode; + + @BeforeTest + public void initializeResponseNode() throws IOException { + + ClassPathResource resource = + new ClassPathResource("org/openmhealth/shim/misfit/mapper/misfit-detailed-summaries.json"); + responseNode = objectMapper.readTree(resource.getInputStream()); + } + + @Test + public void asDataPointsShouldReturnCorrectNumberOfDataPoints() { + + List> dataPoints = mapper.asDataPoints(singletonList(responseNode)); + + assertThat(dataPoints, notNullValue()); + assertThat(dataPoints.size(), equalTo(3)); + } + + @Test + public void asDataPointsShouldReturnCorrectDataPoints() { + + List> dataPoints = mapper.asDataPoints(singletonList(responseNode)); + + assertThat(dataPoints, notNullValue()); + assertThat(dataPoints.size(), greaterThan(0)); + + // FIXME fix the time zone offset once Misfit add it to the API + TimeInterval effectiveTimeInterval = TimeInterval.ofStartDateTimeAndDuration( + OffsetDateTime.of(2015, 4, 13, 0, 0, 0, 0, UTC), + new DurationUnitValue(DAY, 1)); + + StepCount stepCount = new StepCount.Builder(26370) + .setEffectiveTimeFrame(effectiveTimeInterval) + .build(); + + DataPoint firstDataPoint = dataPoints.get(0); + + assertThat(firstDataPoint.getBody(), equalTo(stepCount)); + + DataPointAcquisitionProvenance acquisitionProvenance = firstDataPoint.getHeader().getAcquisitionProvenance(); + + assertThat(acquisitionProvenance, notNullValue()); + assertThat(acquisitionProvenance.getSourceName(), equalTo(RESOURCE_API_SOURCE_NAME)); + } + + @Test + public void asDataPointsShouldReturnEmptyListIfEmptyResponse() throws IOException { + + JsonNode emptyNode = objectMapper.readTree("{\n" + + " \"summary\": []\n" + + "}"); + List> dataPoints = mapper.asDataPoints(singletonList(emptyNode)); + + assertThat(dataPoints.size(), Matchers.equalTo(0)); + } +} \ No newline at end of file diff --git a/shim-server/src/test/java/org/openmhealth/shim/runkeeper/RunkeeperShimTest.java b/shim-server/src/test/java/org/openmhealth/shim/runkeeper/RunkeeperShimTest.java deleted file mode 100644 index 3869137b..00000000 --- a/shim-server/src/test/java/org/openmhealth/shim/runkeeper/RunkeeperShimTest.java +++ /dev/null @@ -1,167 +0,0 @@ -/* - * Copyright 2014 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.runkeeper; - -import com.fasterxml.jackson.databind.ObjectMapper; -import com.fasterxml.jackson.databind.module.SimpleModule; -import com.github.fge.jsonschema.core.exceptions.ProcessingException; -import com.github.fge.jsonschema.core.report.ProcessingReport; -import com.github.fge.jsonschema.main.JsonSchema; -import com.github.fge.jsonschema.main.JsonSchemaFactory; -import org.joda.time.DateTime; -import org.joda.time.DateTimeZone; -import org.joda.time.format.DateTimeFormat; -import org.joda.time.format.DateTimeFormatter; -import org.junit.Test; -import org.openmhealth.schema.pojos.Activity; -import org.openmhealth.schema.pojos.BodyWeight; -import org.openmhealth.schema.pojos.generic.DurationUnitValue; -import org.openmhealth.schema.pojos.generic.LengthUnitValue; -import org.openmhealth.schema.pojos.generic.MassUnitValue; -import org.openmhealth.shim.ShimDataResponse; - -import java.io.IOException; -import java.io.InputStream; -import java.math.BigDecimal; -import java.math.RoundingMode; -import java.net.URL; -import java.util.List; -import java.util.Map; - -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertNotNull; -import static org.junit.Assert.assertTrue; - -/** - * @author Danilo Bonilla - */ -public class RunkeeperShimTest { - - @Test - @SuppressWarnings("unchecked") - public void testActivityNormalize() throws IOException, ProcessingException { - URL url = Thread.currentThread().getContextClassLoader().getResource("runkeeper-activity.json"); - assert url != null; - InputStream inputStream = url.openStream(); - - ObjectMapper objectMapper = new ObjectMapper(); - - RunkeeperShim.RunkeeperDataType.ACTIVITY.getNormalizer(); - SimpleModule module = new SimpleModule(); - module.addDeserializer(ShimDataResponse.class, - RunkeeperShim.RunkeeperDataType.ACTIVITY.getNormalizer()); - - objectMapper.registerModule(module); - - ShimDataResponse response = - objectMapper.readValue(inputStream, ShimDataResponse.class); - - assertNotNull(response); - assertNotNull(response.getShim()); - - Map map = (Map) response.getBody(); - assertTrue(map.containsKey(Activity.SCHEMA_ACTIVITY)); - - List activities = (List) map.get(Activity.SCHEMA_ACTIVITY); - assertTrue(activities != null && activities.size() == 2); - - DateTimeFormatter dateFormatter = - DateTimeFormat.forPattern("EEE, d MMM yyyy HH:mm:ss") - .withZone(DateTimeZone.UTC); - - final String START_TIME_STRING = "Wed, 6 Aug 2014 04:49:00"; - DateTime expectedStartTimeUTC = dateFormatter.parseDateTime(START_TIME_STRING); - - Activity activity = activities.get(0); - assertEquals(activity.getDistance().getValue(), new BigDecimal(6437.3760)); - assertEquals(activity.getDistance().getUnit(), LengthUnitValue.LengthUnit.m); - assertEquals(activity.getActivityName(), "Rowing"); - assertEquals( - activity.getEffectiveTimeFrame().getTimeInterval().getStartTime(), - expectedStartTimeUTC); - assertEquals( - activity.getEffectiveTimeFrame().getTimeInterval().getDuration().getUnit(), - DurationUnitValue.DurationUnit.sec); - assertEquals( - activity.getEffectiveTimeFrame().getTimeInterval().getDuration().getValue(), - new BigDecimal(3600d)); - - /** - * Verify that the output from runkeeper normalizer passes - * a schema check. Per github issue #9. - */ - final String PHYSICAL_ACTIVITY_SCHEMA = - "http://www.openmhealth.org/schema/omh/clinical/physical-activity-1.0.json"; - - ObjectMapper mapper = new ObjectMapper(); - - final JsonSchemaFactory factory = JsonSchemaFactory.byDefault(); - final JsonSchema schema = factory.getJsonSchema(PHYSICAL_ACTIVITY_SCHEMA); - - ProcessingReport report; - - String rawJson = mapper.writeValueAsString(activity); - - report = schema.validate(mapper.readTree(rawJson)); - System.out.println(report); - - assertTrue("Expected valid result!", report.isSuccess()); - } - - /*@Test - @SuppressWarnings("unchecked") - public void testWeightNormalize() throws IOException { - URL url = Thread.currentThread().getContextClassLoader().getResource("runkeeper-weight.json"); - assert url != null; - InputStream inputStream = url.openStream(); - - ObjectMapper objectMapper = new ObjectMapper(); - - RunkeeperShim.RunkeeperDataType.WEIGHT.getNormalizer(); - SimpleModule module = new SimpleModule(); - module.addDeserializer(ShimDataResponse.class, - RunkeeperShim.RunkeeperDataType.WEIGHT.getNormalizer()); - - objectMapper.registerModule(module); - - ShimDataResponse response = - objectMapper.readValue(inputStream, ShimDataResponse.class); - - assertNotNull(response); - - Map map = (Map) response.getBody(); - assertTrue(map.containsKey(BodyWeight.SCHEMA_BODY_WEIGHT)); - - List weights = (List) map.get(BodyWeight.SCHEMA_BODY_WEIGHT); - assertTrue(weights != null && weights.size() == 2); - - DateTimeFormatter dateFormatter = - DateTimeFormat.forPattern("EEE, d MMM yyyy HH:mm:ss") - .withZone(DateTimeZone.UTC); - - final String START_TIME_STRING = "Sat, 9 Aug 2014 04:46:47"; - DateTime expectedStartTimeUTC = dateFormatter.parseDateTime(START_TIME_STRING); - - BodyWeight bodyWeight = weights.get(0); - assertEquals(bodyWeight.getMassUnitValue().getValue().setScale(3, RoundingMode.HALF_DOWN), - new BigDecimal(81.6466265999547d).setScale(3, RoundingMode.HALF_DOWN)); - assertEquals(bodyWeight.getMassUnitValue().getUnit(), MassUnitValue.MassUnit.kg); - assertEquals( - bodyWeight.getEffectiveTimeFrame().getDateTime(), - expectedStartTimeUTC); - }*/ -} diff --git a/shim-server/src/test/java/org/openmhealth/shim/runkeeper/mapper/RunkeeperCaloriesBurnedDataPointMapperUnitTests.java b/shim-server/src/test/java/org/openmhealth/shim/runkeeper/mapper/RunkeeperCaloriesBurnedDataPointMapperUnitTests.java new file mode 100644 index 00000000..b0d1c481 --- /dev/null +++ b/shim-server/src/test/java/org/openmhealth/shim/runkeeper/mapper/RunkeeperCaloriesBurnedDataPointMapperUnitTests.java @@ -0,0 +1,75 @@ +package org.openmhealth.shim.runkeeper.mapper; + +import com.fasterxml.jackson.databind.JsonNode; +import org.openmhealth.schema.domain.omh.*; +import org.openmhealth.shim.common.mapper.DataPointMapperUnitTests; +import org.springframework.core.io.ClassPathResource; +import org.testng.annotations.BeforeTest; +import org.testng.annotations.Test; + +import java.io.IOException; +import java.time.OffsetDateTime; +import java.util.List; + +import static java.util.Collections.singletonList; +import static org.hamcrest.CoreMatchers.equalTo; +import static org.hamcrest.MatcherAssert.assertThat; + + +/** + * @author Chris Schaefbauer + */ +public class RunkeeperCaloriesBurnedDataPointMapperUnitTests extends DataPointMapperUnitTests { + + private JsonNode responseNode; + private RunkeeperCaloriesBurnedDataPointMapper mapper = new RunkeeperCaloriesBurnedDataPointMapper(); + + @BeforeTest + public void initializeResponseNode() throws IOException { + + ClassPathResource resource = + new ClassPathResource("org/openmhealth/shim/runkeeper/mapper/runkeeper-fitness-activities.json"); + responseNode = objectMapper.readTree(resource.getInputStream()); + } + + @Test + public void asDataPointsShouldReturnCorrectNumberOfDataPoints() { + + List> dataPoints = mapper.asDataPoints(singletonList(responseNode)); + + assertThat(dataPoints.size(), equalTo(1)); + } + + @Test + public void asDataPointsShouldReturnCorrectDataPointBodies() { + + List> dataPoints = mapper.asDataPoints(singletonList(responseNode)); + + CaloriesBurned.Builder expectedCaloriesBurnedBuilder = + new CaloriesBurned.Builder(new KcalUnitValue(KcalUnit.KILOCALORIE, 210.796359954334)) + .setActivityName("Cycling") + .setEffectiveTimeFrame(TimeInterval.ofStartDateTimeAndDuration( + OffsetDateTime.parse("2014-10-19T13:17:27+02:00"), + new DurationUnitValue(DurationUnit.SECOND, 4364.74158141667))); + + assertThat(dataPoints.get(0).getBody(), equalTo(expectedCaloriesBurnedBuilder.build())); + } + + @Test + public void asDataPointsShouldReturnCorrectDataPointHeaders() { + + List> dataPoints = mapper.asDataPoints(singletonList(responseNode)); + DataPointHeader firstTestHeader = dataPoints.get(0).getHeader(); + + assertThat(firstTestHeader.getAcquisitionProvenance().getModality(), equalTo(DataPointModality.SENSED)); + + assertThat(firstTestHeader.getBodySchemaId(), equalTo(CaloriesBurned.SCHEMA_ID)); + + assertThat(firstTestHeader.getAcquisitionProvenance().getAdditionalProperty("external_id").get(), + equalTo("/fitnessActivities/465161536")); + + assertThat(firstTestHeader.getAcquisitionProvenance().getSourceName(), + equalTo(RunkeeperDataPointMapper.RESOURCE_API_SOURCE_NAME)); + + } +} diff --git a/shim-server/src/test/java/org/openmhealth/shim/runkeeper/mapper/RunkeeperDataPointMapperUnitTests.java b/shim-server/src/test/java/org/openmhealth/shim/runkeeper/mapper/RunkeeperDataPointMapperUnitTests.java new file mode 100644 index 00000000..329a65bb --- /dev/null +++ b/shim-server/src/test/java/org/openmhealth/shim/runkeeper/mapper/RunkeeperDataPointMapperUnitTests.java @@ -0,0 +1,79 @@ +package org.openmhealth.shim.runkeeper.mapper; + +import com.fasterxml.jackson.databind.JsonNode; +import org.openmhealth.schema.domain.omh.DataPoint; +import org.openmhealth.schema.domain.omh.DataPointModality; +import org.openmhealth.shim.common.mapper.DataPointMapperUnitTests; +import org.testng.annotations.Test; + +import java.io.IOException; +import java.util.Optional; + +import static org.hamcrest.CoreMatchers.equalTo; +import static org.hamcrest.MatcherAssert.assertThat; + + +/** + * TODO refactor + * @author Chris Schaefbauer + */ +public class RunkeeperDataPointMapperUnitTests extends DataPointMapperUnitTests { + + RunkeeperDataPointMapper mapper = new RunkeeperDataPointMapper() { + @Override + protected Optional asDataPoint(JsonNode listEntryNode) { + return null; + } + }; + + @Test + public void getModalityShouldReturnSensedOnlyWhenSourceIsRunkeeperAndModeIsApiAndHasPath() throws IOException { + + JsonNode expectedSensedNode = objectMapper.readTree("{\"entry_mode\": \"API\",\n" + + " \"has_path\": true,\n" + + " \"source\": \"RunKeeper\"}"); + + assertThat(mapper.getModality(expectedSensedNode).get(), equalTo(DataPointModality.SENSED)); + + JsonNode expectedEmptyNodeMissingMode = objectMapper.readTree("{\"has_path\": true,\n" + + " \"source\": \"RunKeeper\"}"); + + assertThat(mapper.getModality(expectedEmptyNodeMissingMode).isPresent(), equalTo(false)); + + JsonNode expectedEmptyNodeMissingSource = objectMapper.readTree("{\"has_path\": true,\n" + + " \"source\": \"RunKeeper\"}"); + + assertThat(mapper.getModality(expectedEmptyNodeMissingSource).isPresent(), equalTo(false)); + + JsonNode expectedEmptyNodeMissingHasPath = objectMapper.readTree("{\"entry_mode\": \"API\",\n" + + " \"source\": \"RunKeeper\"}"); + + assertThat(mapper.getModality(expectedEmptyNodeMissingHasPath).isPresent(), equalTo(false)); + + JsonNode expectedEmptyNodeHasPathFalse = objectMapper.readTree("{\"entry_mode\": \"API\",\n" + + " \"has_path\": false,\n" + + " \"source\": \"RunKeeper\"}"); + + assertThat(mapper.getModality(expectedEmptyNodeHasPathFalse).isPresent(), equalTo(false)); + } + + @Test + public void getModalityShouldReturnSelfReportedModeIsWeb() throws IOException { + + JsonNode expectedSensedNode = objectMapper.readTree("{\"entry_mode\": \"Web\",\n" + + " \"has_path\": true,\n" + + " \"source\": \"RunKeeper\"}"); + + assertThat(mapper.getModality(expectedSensedNode).get(), equalTo(DataPointModality.SELF_REPORTED)); + } + + @Test + public void getModalityShouldReturnEmptyWhenSourceIsNotRunkeeper() throws IOException { + + JsonNode expectedEmptyNode = objectMapper.readTree("{\"entry_mode\": \"API\",\n" + + " \"has_path\": true,\n" + + " \"source\": \"Linq (QA)\"}"); + + assertThat(mapper.getModality(expectedEmptyNode).isPresent(), equalTo(false)); + } +} diff --git a/shim-server/src/test/java/org/openmhealth/shim/runkeeper/mapper/RunkeeperPhysicalActivityDataPointMapperUnitTests.java b/shim-server/src/test/java/org/openmhealth/shim/runkeeper/mapper/RunkeeperPhysicalActivityDataPointMapperUnitTests.java new file mode 100644 index 00000000..f11b1a01 --- /dev/null +++ b/shim-server/src/test/java/org/openmhealth/shim/runkeeper/mapper/RunkeeperPhysicalActivityDataPointMapperUnitTests.java @@ -0,0 +1,114 @@ +package org.openmhealth.shim.runkeeper.mapper; + +import com.fasterxml.jackson.databind.JsonNode; +import org.openmhealth.schema.domain.omh.*; +import org.openmhealth.shim.common.mapper.DataPointMapperUnitTests; +import org.springframework.core.io.ClassPathResource; +import org.testng.annotations.BeforeTest; +import org.testng.annotations.Test; + +import java.io.IOException; +import java.time.OffsetDateTime; +import java.time.ZoneOffset; +import java.util.List; + +import static java.util.Collections.singletonList; +import static org.hamcrest.CoreMatchers.equalTo; +import static org.hamcrest.CoreMatchers.notNullValue; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.greaterThan; +import static org.openmhealth.schema.domain.omh.DataPointModality.SELF_REPORTED; +import static org.openmhealth.schema.domain.omh.DataPointModality.SENSED; +import static org.openmhealth.schema.domain.omh.DurationUnit.SECOND; +import static org.openmhealth.schema.domain.omh.LengthUnit.METER; +import static org.openmhealth.shim.runkeeper.mapper.RunkeeperDataPointMapper.RESOURCE_API_SOURCE_NAME; + + +/** + * @author Emerson Farrugia + */ +public class RunkeeperPhysicalActivityDataPointMapperUnitTests extends DataPointMapperUnitTests { + + private final RunkeeperPhysicalActivityDataPointMapper mapper = new RunkeeperPhysicalActivityDataPointMapper(); + + private JsonNode responseNode; + + + @BeforeTest + public void initializeResponseNode() throws IOException { + + ClassPathResource resource = + new ClassPathResource("org/openmhealth/shim/runkeeper/mapper/runkeeper-fitness-activities.json"); + + responseNode = objectMapper.readTree(resource.getInputStream()); + } + + @Test + public void asDataPointsShouldReturnCorrectNumberOfDataPoints() { + + List> dataPoints = mapper.asDataPoints(singletonList(responseNode)); + + assertThat(dataPoints, notNullValue()); + assertThat(dataPoints.size(), equalTo(2)); + } + + @Test + public void asDataPointsShouldReturnCorrectSensedDataPoints() { + + List> dataPoints = mapper.asDataPoints(singletonList(responseNode)); + + assertThat(dataPoints, notNullValue()); + assertThat(dataPoints.size(), greaterThan(0)); + + TimeInterval effectiveTimeInterval = TimeInterval.ofStartDateTimeAndDuration( + OffsetDateTime.of(2014, 10, 19, 13, 17, 27, 0, ZoneOffset.ofHours(2)), + new DurationUnitValue(SECOND, 4364.74158141667)); + + PhysicalActivity physicalActivity = new PhysicalActivity.Builder("Cycling") + .setDistance(new LengthUnitValue(METER, 10128.7131871337)) + .setEffectiveTimeFrame(effectiveTimeInterval) + .build(); + + DataPoint dataPoint = dataPoints.get(0); + + assertThat(dataPoint.getBody(), equalTo(physicalActivity)); + + DataPointAcquisitionProvenance acquisitionProvenance = dataPoint.getHeader().getAcquisitionProvenance(); + + assertThat(acquisitionProvenance, notNullValue()); + assertThat(acquisitionProvenance.getSourceName(), equalTo(RESOURCE_API_SOURCE_NAME)); + assertThat(acquisitionProvenance.getModality(), equalTo(SENSED)); + assertThat(acquisitionProvenance.getAdditionalProperty("external_id").isPresent(), equalTo(true)); + assertThat(acquisitionProvenance.getAdditionalProperty("external_id").get(), + equalTo("/fitnessActivities/465161536")); + } + + @Test + public void asDataPointsShouldReturnCorrectSelfReportedDataPoints() { + + List> dataPoints = mapper.asDataPoints(singletonList(responseNode)); + + assertThat(dataPoints, notNullValue()); + assertThat(dataPoints.size(), equalTo(2)); + + DataPoint dataPoint = dataPoints.get(1); + + DataPointAcquisitionProvenance acquisitionProvenance = dataPoint.getHeader().getAcquisitionProvenance(); + + assertThat(acquisitionProvenance, notNullValue()); + assertThat(acquisitionProvenance.getModality(), equalTo(SELF_REPORTED)); + } + + @Test + public void asDataPointsShouldNotCreateDataPointWhenOffsetMissing() throws IOException { + + ClassPathResource resource = + new ClassPathResource( + "org/openmhealth/shim/runkeeper/mapper/runkeeper-fitness-activities-no-offset.json"); + JsonNode responseNodeWithoutUtcOffset = objectMapper.readTree(resource.getInputStream()); + + List> dataPoints = mapper.asDataPoints(singletonList(responseNodeWithoutUtcOffset)); + + assertThat(dataPoints.size(), equalTo(0)); + } +} \ No newline at end of file diff --git a/shim-server/src/test/java/org/openmhealth/shim/withings/WithingsShimTest.java b/shim-server/src/test/java/org/openmhealth/shim/withings/WithingsShimTest.java deleted file mode 100644 index f4b13fc8..00000000 --- a/shim-server/src/test/java/org/openmhealth/shim/withings/WithingsShimTest.java +++ /dev/null @@ -1,87 +0,0 @@ -/* - * Copyright 2014 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; - -import com.fasterxml.jackson.databind.ObjectMapper; -import com.fasterxml.jackson.databind.module.SimpleModule; -import org.joda.time.DateTime; -import org.joda.time.DateTimeZone; -import org.junit.Test; -import org.openmhealth.schema.pojos.Activity; -import org.openmhealth.schema.pojos.BloodPressure; -import org.openmhealth.schema.pojos.BloodPressureUnit; -import org.openmhealth.shim.ShimDataResponse; -import org.openmhealth.shim.ShimDataType; -import org.openmhealth.shim.runkeeper.RunkeeperShim; - -import java.io.IOException; -import java.io.InputStream; -import java.math.BigDecimal; -import java.net.URL; -import java.util.List; -import java.util.Map; - -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertNotNull; -import static org.junit.Assert.assertTrue; - -/** - * @author Danilo Bonilla - */ -public class WithingsShimTest { - - @Test - @SuppressWarnings("unchecked") - public void testNormalizeBody() throws IOException { - - URL url = Thread.currentThread().getContextClassLoader().getResource("withings-body.json"); - assert url != null; - InputStream inputStream = url.openStream(); - - ObjectMapper objectMapper = new ObjectMapper(); - - WithingsShim.WithingsDataType.BODY.getNormalizer(); - SimpleModule module = new SimpleModule(); - module.addDeserializer(ShimDataResponse.class, - WithingsShim.WithingsDataType.BODY.getNormalizer()); - - objectMapper.registerModule(module); - - ShimDataResponse response = - objectMapper.readValue(inputStream, ShimDataResponse.class); - - assertNotNull(response); - assertNotNull(response.getShim()); - - Map map = (Map) response.getBody(); - assertTrue(map.containsKey(BloodPressure.SCHEMA_BLOOD_PRESSURE)); - - List bloodPressures = (List) map.get(BloodPressure.SCHEMA_BLOOD_PRESSURE); - assertTrue(bloodPressures != null && bloodPressures.size() == 2); - - BloodPressure bloodPressure = bloodPressures.get(0); - - DateTime expectedDateTime = new DateTime(1408276657l*1000l, DateTimeZone.UTC); - - assertEquals(expectedDateTime,bloodPressure.getEffectiveTimeFrame().getDateTime()); - assertEquals(bloodPressure.getDiastolic().getValue(),new BigDecimal(75d)); - assertEquals(bloodPressure.getDiastolic().getUnit(), BloodPressureUnit.mmHg); - assertEquals(bloodPressure.getSystolic().getValue(),new BigDecimal(133d)); - assertEquals(bloodPressure.getSystolic().getUnit(), BloodPressureUnit.mmHg); - } - -} diff --git a/shim-server/src/test/java/org/openmhealth/shim/withings/mapper/WithingsBloodPressureDataPointMapperUnitTests.java b/shim-server/src/test/java/org/openmhealth/shim/withings/mapper/WithingsBloodPressureDataPointMapperUnitTests.java new file mode 100644 index 00000000..5b15ee11 --- /dev/null +++ b/shim-server/src/test/java/org/openmhealth/shim/withings/mapper/WithingsBloodPressureDataPointMapperUnitTests.java @@ -0,0 +1,82 @@ +/* + * 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.withings.mapper; + +import com.fasterxml.jackson.databind.JsonNode; +import org.openmhealth.schema.domain.omh.*; +import org.openmhealth.shim.common.mapper.DataPointMapperUnitTests; +import org.springframework.core.io.ClassPathResource; +import org.testng.annotations.BeforeTest; +import org.testng.annotations.Test; + +import java.io.IOException; +import java.time.OffsetDateTime; +import java.util.List; + +import static java.util.Collections.singletonList; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.equalTo; +import static org.openmhealth.schema.domain.omh.BloodPressureUnit.MM_OF_MERCURY; +import static org.openmhealth.schema.domain.omh.DataPointModality.SENSED; +import static org.openmhealth.shim.withings.mapper.WithingsDataPointMapper.RESOURCE_API_SOURCE_NAME; + + +/** + * @author Chris Schaefbauer + */ +public class WithingsBloodPressureDataPointMapperUnitTests extends DataPointMapperUnitTests { + + private WithingsBloodPressureDataPointMapper mapper = new WithingsBloodPressureDataPointMapper(); + private JsonNode responseNode; + + + @BeforeTest + public void initializeResponseNode() throws IOException { + ClassPathResource resource = + new ClassPathResource("org/openmhealth/shim/withings/mapper/withings-body-measures.json"); + + responseNode = objectMapper.readTree(resource.getInputStream()); + } + + @Test + public void asDataPointsShouldReturnCorrectNumberOfDataPoints() { + + List> dataPoints = mapper.asDataPoints(singletonList(responseNode)); + assertThat(dataPoints.size(), equalTo(1)); + } + + @Test + public void asDataPointsShouldReturnCorrectDataPoints() { + + List> actualDataPoints = mapper.asDataPoints(singletonList(responseNode)); + + BloodPressure expectedBloodPressure = new BloodPressure.Builder( + new SystolicBloodPressure(MM_OF_MERCURY, 104.0), + new DiastolicBloodPressure(MM_OF_MERCURY, 68.0)) + .setEffectiveTimeFrame(OffsetDateTime.parse("2015-05-31T06:06:23Z")) + .build(); + + assertThat(actualDataPoints.get(0).getBody(), equalTo(expectedBloodPressure)); + + DataPointHeader actualDataPointHeader = actualDataPoints.get(0).getHeader(); + assertThat(actualDataPointHeader.getBodySchemaId(), equalTo(BloodPressure.SCHEMA_ID)); + assertThat(actualDataPointHeader.getAcquisitionProvenance().getModality(), equalTo(SENSED)); + assertThat(actualDataPointHeader.getAcquisitionProvenance().getSourceName(), equalTo(RESOURCE_API_SOURCE_NAME)); + assertThat(actualDataPointHeader.getAcquisitionProvenance().getAdditionalProperties().get("external_id"), + equalTo(366956482L)); + } +} diff --git a/shim-server/src/test/java/org/openmhealth/shim/withings/mapper/WithingsBodyHeightDataPointMapperUnitTests.java b/shim-server/src/test/java/org/openmhealth/shim/withings/mapper/WithingsBodyHeightDataPointMapperUnitTests.java new file mode 100644 index 00000000..a01f678c --- /dev/null +++ b/shim-server/src/test/java/org/openmhealth/shim/withings/mapper/WithingsBodyHeightDataPointMapperUnitTests.java @@ -0,0 +1,102 @@ +/* + * 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.withings.mapper; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import org.openmhealth.schema.domain.omh.*; +import org.openmhealth.shim.common.mapper.DataPointMapperUnitTests; +import org.openmhealth.shim.common.mapper.JsonNodeMappingException; +import org.springframework.core.io.ClassPathResource; +import org.testng.annotations.BeforeTest; +import org.testng.annotations.Test; + +import java.io.IOException; +import java.time.OffsetDateTime; +import java.util.List; + +import static java.util.Collections.singletonList; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.equalTo; +import static org.openmhealth.schema.domain.omh.DataPointModality.SELF_REPORTED; +import static org.openmhealth.shim.withings.domain.WithingsBodyMeasureType.BODY_HEIGHT; +import static org.openmhealth.shim.withings.mapper.WithingsDataPointMapper.RESOURCE_API_SOURCE_NAME; + + +/** + * @author Chris Schaefbauer + * @author Emerson Farrugia + */ +public class WithingsBodyHeightDataPointMapperUnitTests extends DataPointMapperUnitTests { + + private static final ObjectMapper objectMapper = new ObjectMapper(); + private WithingsBodyHeightDataPointMapper mapper = new WithingsBodyHeightDataPointMapper(); + private JsonNode responseNode; + + + @BeforeTest + public void initializeResponseNode() throws IOException { + ClassPathResource resource = + new ClassPathResource("org/openmhealth/shim/withings/mapper/withings-body-measures.json"); + + responseNode = objectMapper.readTree(resource.getInputStream()); + } + + // this is included in only one mapper for brevity, can be rinsed and repeated in others if necessary + @Test(expectedExceptions = JsonNodeMappingException.class) + public void getValueForMeasureTypeShouldThrowExceptionOnDuplicateMeasureTypes() throws Exception { + + JsonNode measuresNode = objectMapper.readTree("[\n" + + " {\n" + + " \"type\": 4,\n" + // WithingsBodyMeasureType.BODY_HEIGHT + " \"unit\": 0,\n" + + " \"value\": 68\n" + + " },\n" + + " {\n" + + " \"type\": 4,\n" + + " \"unit\": 0,\n" + + " \"value\": 104\n" + + " }\n" + + "]"); + + mapper.getValueForMeasureType(measuresNode, BODY_HEIGHT); + } + + @Test + public void asDataPointsShouldReturnCorrectNumberOfDataPoints() { + List> dataPoints = mapper.asDataPoints(singletonList(responseNode)); + assertThat(dataPoints.size(), equalTo(1)); + } + + @Test + public void asDataPointsShouldReturnCorrectDataPoints() { + List> actualDataPoints = mapper.asDataPoints(singletonList(responseNode)); + + BodyHeight expectedBodyHeight = new BodyHeight.Builder(new LengthUnitValue(LengthUnit.METER, 1.93)) + .setEffectiveTimeFrame(OffsetDateTime.parse("2015-02-23T19:24:49Z")) + .build(); + + assertThat(actualDataPoints.get(0).getBody(), equalTo(expectedBodyHeight)); + + DataPointHeader actualDataPointHeader = actualDataPoints.get(0).getHeader(); + assertThat(actualDataPointHeader.getBodySchemaId(), equalTo(BodyHeight.SCHEMA_ID)); + assertThat(actualDataPointHeader.getAcquisitionProvenance().getModality(), equalTo(SELF_REPORTED)); + assertThat(actualDataPointHeader.getAcquisitionProvenance().getSourceName(), equalTo(RESOURCE_API_SOURCE_NAME)); + assertThat(actualDataPointHeader.getAcquisitionProvenance().getAdditionalProperties().get("external_id"), + equalTo(320419189L)); + } +} diff --git a/shim-server/src/test/java/org/openmhealth/shim/withings/mapper/WithingsBodyWeightDataPointMapperUnitTests.java b/shim-server/src/test/java/org/openmhealth/shim/withings/mapper/WithingsBodyWeightDataPointMapperUnitTests.java new file mode 100644 index 00000000..762c13cc --- /dev/null +++ b/shim-server/src/test/java/org/openmhealth/shim/withings/mapper/WithingsBodyWeightDataPointMapperUnitTests.java @@ -0,0 +1,93 @@ +/* + * 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.withings.mapper; + +import com.fasterxml.jackson.databind.JsonNode; +import org.openmhealth.schema.domain.omh.*; +import org.openmhealth.shim.common.mapper.DataPointMapperUnitTests; +import org.springframework.core.io.ClassPathResource; +import org.testng.annotations.BeforeTest; +import org.testng.annotations.Test; + +import java.io.IOException; +import java.time.OffsetDateTime; +import java.util.List; + +import static java.util.Collections.singletonList; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.equalTo; +import static org.openmhealth.schema.domain.omh.DataPointModality.SENSED; +import static org.openmhealth.shim.withings.mapper.WithingsDataPointMapper.RESOURCE_API_SOURCE_NAME; + + +// TODO add Javadoc and clean up +public class WithingsBodyWeightDataPointMapperUnitTests extends DataPointMapperUnitTests { + + WithingsBodyWeightDataPointMapper mapper = new WithingsBodyWeightDataPointMapper(); + JsonNode responseNode, responseNodeWithGoal; + + @BeforeTest + public void initializeResponseNode() throws IOException { + + ClassPathResource resource = + new ClassPathResource("org/openmhealth/shim/withings/mapper/withings-body-measures.json"); + responseNode = objectMapper.readTree(resource.getInputStream()); + resource = new ClassPathResource("org/openmhealth/shim/withings/mapper/withings-body-measures-only-goal.json"); + responseNodeWithGoal = objectMapper.readTree(resource.getInputStream()); + } + + @Test + public void asDataPointsShouldReturnCorrectNumberOfDataPoints() { + List> dataPointList = mapper.asDataPoints(singletonList(responseNode)); + assertThat(dataPointList.size(), equalTo(2)); + } + + @Test + public void asDataPointsShouldReturnCorrectDataPoints() { + List> dataPointList = mapper.asDataPoints(singletonList(responseNode)); + + testDataPoint(dataPointList.get(0), 74.126, "2015-05-31T06:06:23Z", 366956482L); + testDataPoint(dataPointList.get(1), 74.128, "2015-04-20T17:13:56Z", 347186704L); + + } + + @Test + public void asDataPointsShouldIgnoreGoalsForBodyMeasures() { + List> dataPoints = mapper.asDataPoints(singletonList(responseNodeWithGoal)); + assertThat(dataPoints.size(), equalTo(0)); + } + + //TODO: Refactor this out with an "expectedProperties" dictionary for all the inputs and then one for all + // Withings points + public void testDataPoint(DataPoint testDataPoint, double massValue, String offsetTimeString, + long externalId) { + BodyWeight.Builder bodyWeightExpectedMeasureBuilder = + new BodyWeight.Builder(new MassUnitValue(MassUnit.KILOGRAM, massValue)); + bodyWeightExpectedMeasureBuilder.setEffectiveTimeFrame(OffsetDateTime.parse(offsetTimeString)); + BodyWeight bodyWeightExpected = bodyWeightExpectedMeasureBuilder.build(); + + assertThat(testDataPoint.getBody(), equalTo(bodyWeightExpected)); + + DataPointAcquisitionProvenance testProvenance = testDataPoint.getHeader().getAcquisitionProvenance(); + assertThat(testProvenance.getSourceName(), equalTo(RESOURCE_API_SOURCE_NAME)); + assertThat(testProvenance.getModality(), equalTo(SENSED)); + Long expectedExternalId = (Long) testDataPoint.getHeader().getAcquisitionProvenance().getAdditionalProperties() + .get("external_id"); + assertThat(expectedExternalId, equalTo(externalId)); + assertThat(testDataPoint.getHeader().getBodySchemaId(), equalTo(BodyWeight.SCHEMA_ID)); + } +} diff --git a/shim-server/src/test/java/org/openmhealth/shim/withings/mapper/WithingsDailyCaloriesBurnedDataPointMapperUnitTests.java b/shim-server/src/test/java/org/openmhealth/shim/withings/mapper/WithingsDailyCaloriesBurnedDataPointMapperUnitTests.java new file mode 100644 index 00000000..8fa59734 --- /dev/null +++ b/shim-server/src/test/java/org/openmhealth/shim/withings/mapper/WithingsDailyCaloriesBurnedDataPointMapperUnitTests.java @@ -0,0 +1,88 @@ +/* + * Copyright 2015 Open mHealth + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.openmhealth.shim.withings.mapper; + +import com.fasterxml.jackson.databind.JsonNode; +import org.openmhealth.schema.domain.omh.*; +import org.openmhealth.shim.common.mapper.DataPointMapperUnitTests; +import org.springframework.core.io.ClassPathResource; +import org.testng.annotations.BeforeTest; +import org.testng.annotations.Test; + +import java.io.IOException; +import java.time.OffsetDateTime; +import java.util.List; + +import static java.util.Collections.singletonList; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.equalTo; +import static org.openmhealth.schema.domain.omh.DataPointModality.SENSED; +import static org.openmhealth.shim.withings.mapper.WithingsDataPointMapper.RESOURCE_API_SOURCE_NAME; + + +// TODO clean up +public class WithingsDailyCaloriesBurnedDataPointMapperUnitTests extends DataPointMapperUnitTests { + + private JsonNode responseNode; + private WithingsDailyCaloriesBurnedDataPointMapper mapper = new WithingsDailyCaloriesBurnedDataPointMapper(); + + @BeforeTest + public void initializeResponseNode() throws IOException { + ClassPathResource resource = + new ClassPathResource("/org/openmhealth/shim/withings/mapper/withings-activity-measures.json"); + responseNode = objectMapper.readTree(resource.getInputStream()); + } + + @Test + public void asDataPointsShouldReturnCorrectNumberOfDataPoints() { + List> dataPoints = mapper.asDataPoints(singletonList(responseNode)); + assertThat(dataPoints.size(), equalTo(4)); + } + + @Test + public void asDataPointsShouldReturnCorrectDataPoints() { + List> dataPoints = mapper.asDataPoints(singletonList(responseNode)); + testDailyCaloriesBurnedDataPoint(dataPoints.get(0), 139, "2015-06-18T00:00:00-07:00", + "2015-06-19T00:00:00-07:00"); + testDailyCaloriesBurnedDataPoint(dataPoints.get(1), 130, "2015-06-19T00:00:00-07:00", + "2015-06-20T00:00:00-07:00"); + testDailyCaloriesBurnedDataPoint(dataPoints.get(2), 241, "2015-06-20T00:00:00-07:00", + "2015-06-21T00:00:00-07:00"); + testDailyCaloriesBurnedDataPoint(dataPoints.get(3), 99, "2015-02-21T00:00:00-08:00", + "2015-02-22T00:00:00-08:00"); + + } + + public void testDailyCaloriesBurnedDataPoint(DataPoint caloriesBurnedDataPoint, + long expectedCaloriesBurnedValue, String expectedDateString, String expectedEndDateString) { + CaloriesBurned.Builder expectedCaloriesBurnedBuilder = + new CaloriesBurned.Builder(new KcalUnitValue(KcalUnit.KILOCALORIE, expectedCaloriesBurnedValue)); + expectedCaloriesBurnedBuilder.setEffectiveTimeFrame( + TimeInterval.ofStartDateTimeAndEndDateTime(OffsetDateTime.parse(expectedDateString), + OffsetDateTime.parse(expectedEndDateString))); + CaloriesBurned testCaloriesBurned = caloriesBurnedDataPoint.getBody(); + CaloriesBurned expectedCaloriesBurned = expectedCaloriesBurnedBuilder.build(); + assertThat(testCaloriesBurned, equalTo(expectedCaloriesBurned)); + assertThat(caloriesBurnedDataPoint.getHeader().getAcquisitionProvenance().getModality(), equalTo(SENSED)); + assertThat(caloriesBurnedDataPoint.getHeader().getAcquisitionProvenance().getSourceName(), equalTo( + RESOURCE_API_SOURCE_NAME)); + assertThat(caloriesBurnedDataPoint.getHeader().getBodySchemaId(), equalTo(CaloriesBurned.SCHEMA_ID)); + + } + + +} diff --git a/shim-server/src/test/java/org/openmhealth/shim/withings/mapper/WithingsDailyStepCountDataPointMapperUnitTests.java b/shim-server/src/test/java/org/openmhealth/shim/withings/mapper/WithingsDailyStepCountDataPointMapperUnitTests.java new file mode 100644 index 00000000..dff0e2af --- /dev/null +++ b/shim-server/src/test/java/org/openmhealth/shim/withings/mapper/WithingsDailyStepCountDataPointMapperUnitTests.java @@ -0,0 +1,86 @@ +/* + * 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.withings.mapper; + +import com.fasterxml.jackson.databind.JsonNode; +import org.openmhealth.schema.domain.omh.DataPoint; +import org.openmhealth.schema.domain.omh.StepCount; +import org.openmhealth.schema.domain.omh.TimeInterval; +import org.openmhealth.shim.common.mapper.DataPointMapperUnitTests; +import org.springframework.core.io.ClassPathResource; +import org.testng.annotations.BeforeTest; +import org.testng.annotations.Test; + +import java.io.IOException; +import java.time.OffsetDateTime; +import java.util.List; + +import static java.util.Collections.singletonList; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.equalTo; +import static org.openmhealth.schema.domain.omh.DataPointModality.SENSED; +import static org.openmhealth.shim.withings.mapper.WithingsDataPointMapper.RESOURCE_API_SOURCE_NAME; + + +// TODO clean up +public class WithingsDailyStepCountDataPointMapperUnitTests extends DataPointMapperUnitTests { + + + private JsonNode responseNode; + private WithingsDailyStepCountDataPointMapper mapper = new WithingsDailyStepCountDataPointMapper(); + + @BeforeTest + public void initializeResponseNode() throws IOException { + ClassPathResource resource = + new ClassPathResource("/org/openmhealth/shim/withings/mapper/withings-activity-measures.json"); + responseNode = objectMapper.readTree(resource.getInputStream()); + } + + @Test + public void asDataPointsReturnsCorrectNumberOfDataPoints() { + List> dataPoints = mapper.asDataPoints(singletonList(responseNode)); + assertThat(dataPoints.size(), equalTo(4)); + + } + + @Test + public void asDataPointsReturnsCorrectDataPoints() { + List> dataPoints = mapper.asDataPoints(singletonList(responseNode)); + testDailyStepCountDataPoint(dataPoints.get(0), 2934, "2015-06-18T00:00:00-07:00", "2015-06-19T00:00:00-07:00"); + testDailyStepCountDataPoint(dataPoints.get(1), 2600, "2015-06-19T00:00:00-07:00", "2015-06-20T00:00:00-07:00"); + testDailyStepCountDataPoint(dataPoints.get(2), 5458, "2015-06-20T00:00:00-07:00", "2015-06-21T00:00:00-07:00"); + testDailyStepCountDataPoint(dataPoints.get(3), 1798, "2015-02-21T00:00:00-08:00", "2015-02-22T00:00:00-08:00"); + } + + public void testDailyStepCountDataPoint(DataPoint stepCountDataPoint, long expectedStepCountValue, + String expectedDateString, String expectedEndDateString) { + StepCount.Builder expectedStepCountBuilder = new StepCount.Builder(expectedStepCountValue); + expectedStepCountBuilder.setEffectiveTimeFrame(TimeInterval + .ofStartDateTimeAndEndDateTime(OffsetDateTime.parse(expectedDateString), + OffsetDateTime.parse(expectedEndDateString))); + //ofStartDateTimeAndDuration(OffsetDateTime.parse(expectedDateString),new DurationUnitValue( + //DurationUnit.DAY, 1))); + StepCount testStepCount = stepCountDataPoint.getBody(); + StepCount expectedStepCount = expectedStepCountBuilder.build(); + assertThat(testStepCount, equalTo(expectedStepCount)); + assertThat(stepCountDataPoint.getHeader().getAcquisitionProvenance().getModality(), equalTo(SENSED)); + assertThat(stepCountDataPoint.getHeader().getAcquisitionProvenance().getSourceName(), equalTo( + RESOURCE_API_SOURCE_NAME)); + assertThat(stepCountDataPoint.getHeader().getBodySchemaId(), equalTo(StepCount.SCHEMA_ID)); + + } +} diff --git a/shim-server/src/test/java/org/openmhealth/shim/withings/mapper/WithingsHeartRateDataPointMapperUnitTests.java b/shim-server/src/test/java/org/openmhealth/shim/withings/mapper/WithingsHeartRateDataPointMapperUnitTests.java new file mode 100644 index 00000000..880dc62e --- /dev/null +++ b/shim-server/src/test/java/org/openmhealth/shim/withings/mapper/WithingsHeartRateDataPointMapperUnitTests.java @@ -0,0 +1,106 @@ +/* + * 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.withings.mapper; + +import com.fasterxml.jackson.databind.JsonNode; +import org.openmhealth.schema.domain.omh.DataPoint; +import org.openmhealth.schema.domain.omh.DataPointAcquisitionProvenance; +import org.openmhealth.schema.domain.omh.DataPointModality; +import org.openmhealth.schema.domain.omh.HeartRate; +import org.openmhealth.shim.common.mapper.DataPointMapperUnitTests; +import org.springframework.core.io.ClassPathResource; +import org.testng.annotations.BeforeTest; +import org.testng.annotations.Test; + +import java.io.IOException; +import java.time.OffsetDateTime; +import java.util.List; + +import static java.util.Collections.singletonList; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.equalTo; +import static org.openmhealth.schema.domain.omh.DataPointModality.SELF_REPORTED; +import static org.openmhealth.schema.domain.omh.DataPointModality.SENSED; +import static org.openmhealth.shim.withings.mapper.WithingsDataPointMapper.RESOURCE_API_SOURCE_NAME; + + +/** + * @author Chris Schaefbauer + * @author Emerson Farrugia + */ +public class WithingsHeartRateDataPointMapperUnitTests extends DataPointMapperUnitTests { + + private WithingsHeartRateDataPointMapper mapper = new WithingsHeartRateDataPointMapper(); + protected JsonNode responseNode; + + + @BeforeTest + public void initializeDataPoints() throws IOException { + + ClassPathResource resource = + new ClassPathResource("/org/openmhealth/shim/withings/mapper/withings-body-measures.json"); + + responseNode = objectMapper.readTree(resource.getInputStream()); + } + + @Test + public void asDataPointsShouldReturnCorrectNumberOfDataPoints() { + + List> dataPoints = mapper.asDataPoints(singletonList(responseNode)); + + assertThat(dataPoints.size(), equalTo(3)); + } + + @Test + public void asDataPointsShouldReturnCorrectSensedDataPoint() { + + List> dataPoints = mapper.asDataPoints(singletonList(responseNode)); + + assertThatDataPointMatches(dataPoints.get(0), 41.0, "2015-05-31T06:06:23Z", 366956482L, null, SENSED); + } + + @Test + public void asDataPointsShouldReturnCorrectSelfReportedDataPoint() { + + List> dataPoints = mapper.asDataPoints(singletonList(responseNode)); + + assertThatDataPointMatches(dataPoints.get(2), 47.0, "2015-02-26T21:57:17Z", 321858727L, + "a few minutes after a walk", SELF_REPORTED); + } + + private void assertThatDataPointMatches(DataPoint actualDataPoint, double expectedHeartRateValue, + String expectedDateTimeAsString, long expectedExternalId, String expectedComment, + DataPointModality expectedModality) { + + HeartRate.Builder expectedHeartRateBuilder = new HeartRate.Builder(expectedHeartRateValue); + expectedHeartRateBuilder.setEffectiveTimeFrame(OffsetDateTime.parse(expectedDateTimeAsString)); + + if (expectedComment != null) { + expectedHeartRateBuilder.setUserNotes(expectedComment); + } + + HeartRate expectedHeartRate = expectedHeartRateBuilder.build(); + + assertThat(actualDataPoint.getBody(), equalTo(expectedHeartRate)); + assertThat(actualDataPoint.getHeader().getBodySchemaId(), equalTo(HeartRate.SCHEMA_ID)); + + DataPointAcquisitionProvenance actualProvenance = actualDataPoint.getHeader().getAcquisitionProvenance(); + assertThat(actualProvenance.getModality(), equalTo(expectedModality)); + assertThat(actualProvenance.getSourceName(), equalTo(RESOURCE_API_SOURCE_NAME)); + assertThat(actualProvenance.getAdditionalProperties().get("external_id"), equalTo(expectedExternalId)); + } +} diff --git a/shim-server/src/test/java/org/openmhealth/shim/withings/mapper/WithingsIntradayCaloriesBurnedDataPointMapperUnitTests.java b/shim-server/src/test/java/org/openmhealth/shim/withings/mapper/WithingsIntradayCaloriesBurnedDataPointMapperUnitTests.java new file mode 100644 index 00000000..c125058e --- /dev/null +++ b/shim-server/src/test/java/org/openmhealth/shim/withings/mapper/WithingsIntradayCaloriesBurnedDataPointMapperUnitTests.java @@ -0,0 +1,89 @@ +/* + * 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.withings.mapper; + +import com.fasterxml.jackson.databind.JsonNode; +import org.openmhealth.schema.domain.omh.*; +import org.openmhealth.shim.common.mapper.DataPointMapperUnitTests; +import org.springframework.core.io.ClassPathResource; +import org.testng.annotations.BeforeTest; +import org.testng.annotations.Test; + +import java.io.IOException; +import java.time.OffsetDateTime; +import java.util.List; + +import static java.util.Collections.singletonList; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.equalTo; +import static org.openmhealth.schema.domain.omh.DataPointModality.SENSED; +import static org.openmhealth.shim.withings.mapper.WithingsDataPointMapper.RESOURCE_API_SOURCE_NAME; + + +// TODO clean up +/** + * Created by Chris Schaefbauer on 7/5/15. + */ +public class WithingsIntradayCaloriesBurnedDataPointMapperUnitTests extends DataPointMapperUnitTests { + + private JsonNode responseNode; + private WithingsIntradayCaloriesBurnedDataPointMapper mapper = new WithingsIntradayCaloriesBurnedDataPointMapper(); + + @BeforeTest + public void initializeResponseNode() throws IOException { + ClassPathResource resource = + new ClassPathResource("/org/openmhealth/shim/withings/mapper/withings-intraday-activity.json"); + responseNode = objectMapper.readTree(resource.getInputStream()); + } + + @Test + public void asDataPointsShouldReturnCorrectNumberOfDataPoints() { + List> dataPoints = mapper.asDataPoints(singletonList(responseNode)); + assertThat(dataPoints.size(), equalTo(4)); + } + + @Test + public void asDataPointsShouldReturnCorrectDataPoints() { + List> dataPoints = mapper.asDataPoints(singletonList(responseNode)); + testIntradayCaloriesBurnedDataPoint(dataPoints.get(0), 1, "2015-06-20T00:04:00Z", 60L); + testIntradayCaloriesBurnedDataPoint(dataPoints.get(1), 2, "2015-06-20T00:29:00Z", 60L); + testIntradayCaloriesBurnedDataPoint(dataPoints.get(2), 1, "2015-06-20T00:30:00Z", 60L); + testIntradayCaloriesBurnedDataPoint(dataPoints.get(3), 7, "2015-06-20T00:41:00Z", 60L); + + + } + + public void testIntradayCaloriesBurnedDataPoint(DataPoint caloriesBurnedDataPoint, + long expectedCaloriesBurnedValue, + String expectedDateString, Long expectedDuration) { + CaloriesBurned.Builder expectedCaloriesBurnedBuilder = + new CaloriesBurned.Builder(new KcalUnitValue(KcalUnit.KILOCALORIE, expectedCaloriesBurnedValue)); + + expectedCaloriesBurnedBuilder.setEffectiveTimeFrame( + TimeInterval.ofStartDateTimeAndDuration(OffsetDateTime.parse(expectedDateString), new DurationUnitValue( + DurationUnit.SECOND, expectedDuration))); + CaloriesBurned testCaloriesBurned = caloriesBurnedDataPoint.getBody(); + CaloriesBurned expectedCaloriesBurned = expectedCaloriesBurnedBuilder.build(); + assertThat(testCaloriesBurned, equalTo(expectedCaloriesBurned)); + assertThat(caloriesBurnedDataPoint.getHeader().getAcquisitionProvenance().getModality(), equalTo(SENSED)); + assertThat(caloriesBurnedDataPoint.getHeader().getAcquisitionProvenance().getSourceName(), equalTo( + RESOURCE_API_SOURCE_NAME)); + assertThat(caloriesBurnedDataPoint.getHeader().getBodySchemaId(), equalTo(CaloriesBurned.SCHEMA_ID)); + + } + +} diff --git a/shim-server/src/test/java/org/openmhealth/shim/withings/mapper/WithingsIntradayStepCountDataPointMapperUnitTests.java b/shim-server/src/test/java/org/openmhealth/shim/withings/mapper/WithingsIntradayStepCountDataPointMapperUnitTests.java new file mode 100644 index 00000000..784d7921 --- /dev/null +++ b/shim-server/src/test/java/org/openmhealth/shim/withings/mapper/WithingsIntradayStepCountDataPointMapperUnitTests.java @@ -0,0 +1,86 @@ +/* + * 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.withings.mapper; + +import com.fasterxml.jackson.databind.JsonNode; +import org.openmhealth.schema.domain.omh.*; +import org.openmhealth.shim.common.mapper.DataPointMapperUnitTests; +import org.springframework.core.io.ClassPathResource; +import org.testng.annotations.BeforeTest; +import org.testng.annotations.Test; + +import java.io.IOException; +import java.time.OffsetDateTime; +import java.util.List; + +import static java.util.Collections.singletonList; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.equalTo; +import static org.openmhealth.schema.domain.omh.DataPointModality.SENSED; +import static org.openmhealth.shim.withings.mapper.WithingsDataPointMapper.RESOURCE_API_SOURCE_NAME; + + +// TODO clean up +/** + * Created by Chris Schaefbauer on 7/2/15. + */ +public class WithingsIntradayStepCountDataPointMapperUnitTests extends DataPointMapperUnitTests { + + protected JsonNode responseNode; + private WithingsIntradayStepCountDataPointMapper mapper = new WithingsIntradayStepCountDataPointMapper(); + + @BeforeTest + public void initializeResponseNode() throws IOException { + ClassPathResource resource = + new ClassPathResource("/org/openmhealth/shim/withings/mapper/withings-intraday-activity.json"); + responseNode = objectMapper.readTree(resource.getInputStream()); + } + + @Test + public void asDataPointsShouldReturnCorrectNumberOfDataPoints() { + List> dataPoints = mapper.asDataPoints(singletonList(responseNode)); + assertThat(dataPoints.size(), equalTo(4)); + + } + + @Test + public void asDataPointsShouldReturnCorrectDataPoints() { + List> dataPoints = mapper.asDataPoints(singletonList(responseNode)); + testIntradayStepCountDataPoint(dataPoints.get(0), 21, "2015-06-20T00:04:00Z", 60L); + testIntradayStepCountDataPoint(dataPoints.get(1), 47, "2015-06-20T00:29:00Z", 60L); + testIntradayStepCountDataPoint(dataPoints.get(2), 20, "2015-06-20T00:30:00Z", 60L); + testIntradayStepCountDataPoint(dataPoints.get(3), 74, "2015-06-20T00:41:00Z", 60L); + } + + public void testIntradayStepCountDataPoint(DataPoint stepCountDataPoint, long expectedStepCountValue, + String expectedDateString, Long expectedDuration) { + StepCount.Builder expectedStepCountBuilder = new StepCount.Builder(expectedStepCountValue); + expectedStepCountBuilder.setEffectiveTimeFrame( + TimeInterval.ofStartDateTimeAndDuration(OffsetDateTime.parse(expectedDateString), new DurationUnitValue( + DurationUnit.SECOND, expectedDuration))); + StepCount testStepCount = stepCountDataPoint.getBody(); + StepCount expectedStepCount = expectedStepCountBuilder.build(); + assertThat(testStepCount, equalTo(expectedStepCount)); + assertThat(stepCountDataPoint.getHeader().getAcquisitionProvenance().getModality(), equalTo(SENSED)); + assertThat(stepCountDataPoint.getHeader().getAcquisitionProvenance().getSourceName(), equalTo( + RESOURCE_API_SOURCE_NAME)); + assertThat(stepCountDataPoint.getHeader().getBodySchemaId(), equalTo(StepCount.SCHEMA_ID)); + + } + + +} diff --git a/shim-server/src/test/java/org/openmhealth/shim/withings/mapper/WithingsSleepDurationDataPointMapperUnitTests.java b/shim-server/src/test/java/org/openmhealth/shim/withings/mapper/WithingsSleepDurationDataPointMapperUnitTests.java new file mode 100644 index 00000000..02985c53 --- /dev/null +++ b/shim-server/src/test/java/org/openmhealth/shim/withings/mapper/WithingsSleepDurationDataPointMapperUnitTests.java @@ -0,0 +1,90 @@ +/* + * 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.withings.mapper; + +import com.fasterxml.jackson.databind.JsonNode; +import org.openmhealth.schema.domain.omh.*; +import org.openmhealth.shim.common.mapper.DataPointMapperUnitTests; +import org.springframework.core.io.ClassPathResource; +import org.testng.annotations.BeforeTest; +import org.testng.annotations.Test; + +import java.io.IOException; +import java.time.OffsetDateTime; +import java.util.List; + +import static java.util.Collections.singletonList; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.equalTo; +import static org.openmhealth.schema.domain.omh.DataPointModality.SENSED; +import static org.openmhealth.schema.domain.omh.DurationUnit.SECOND; + + +// TODO clean this up +/** + * Created by Chris Schaefbauer on 7/6/15. + */ +public class WithingsSleepDurationDataPointMapperUnitTests extends DataPointMapperUnitTests { + + private JsonNode responseNode; + private WithingsSleepDurationDataPointMapper mapper = new WithingsSleepDurationDataPointMapper(); + + @BeforeTest + public void initializeResponseNode() throws IOException { + + ClassPathResource resource = + new ClassPathResource("org/openmhealth/shim/withings/mapper/withings-sleep-summary.json"); + + responseNode = objectMapper.readTree(resource.getInputStream()); + } + + @Test + public void asDataPointsShouldReturnCorrectNumberOfDataPoints() { + + List> dataPoints = mapper.asDataPoints(singletonList(responseNode)); + assertThat(dataPoints.size(), equalTo(1)); + } + + @Test + public void asDataPointsShouldReturnCorrectDataPoints() { + + List> dataPoints = mapper.asDataPoints(singletonList(responseNode)); + SleepDuration.Builder sleepDurationBuilder = new SleepDuration.Builder(new DurationUnitValue(SECOND, 37460)); + + OffsetDateTime offsetStartDateTime = OffsetDateTime.parse("2014-09-12T11:34:19Z"); + OffsetDateTime offsetEndDateTime = OffsetDateTime.parse("2014-09-12T17:22:57Z"); + sleepDurationBuilder.setEffectiveTimeFrame( + TimeInterval.ofStartDateTimeAndEndDateTime(offsetStartDateTime, offsetEndDateTime)); + + SleepDuration expectedSleepDuration = sleepDurationBuilder.build(); + expectedSleepDuration.setAdditionalProperty("wakeup_count", 3); + expectedSleepDuration.setAdditionalProperty("light_sleep_duration", new DurationUnitValue(SECOND, 18540)); + expectedSleepDuration.setAdditionalProperty("deep_sleep_duration", new DurationUnitValue(SECOND, 8460)); + expectedSleepDuration.setAdditionalProperty("rem_sleep_duration", new DurationUnitValue(SECOND, 10460)); + expectedSleepDuration.setAdditionalProperty("duration_to_sleep", new DurationUnitValue(SECOND, 420)); + assertThat(dataPoints.get(0).getBody(), equalTo(expectedSleepDuration)); + + assertThat(dataPoints.get(0).getBody().getAdditionalProperties(), + equalTo(expectedSleepDuration.getAdditionalProperties())); + DataPointAcquisitionProvenance acquisitionProvenance = dataPoints.get(0).getHeader().getAcquisitionProvenance(); + assertThat(acquisitionProvenance.getSourceName(), equalTo(WithingsDataPointMapper.RESOURCE_API_SOURCE_NAME)); + assertThat(acquisitionProvenance.getModality(), equalTo(SENSED)); + assertThat(acquisitionProvenance.getAdditionalProperties().get("external_id"), equalTo(16616514L)); + assertThat(acquisitionProvenance.getAdditionalProperties().get("device_name"), equalTo("Aura")); + assertThat(dataPoints.get(0).getHeader().getBodySchemaId(), equalTo(SleepDuration.SCHEMA_ID)); + } +} diff --git a/shim-server/src/test/resources/fitbit-activity.json b/shim-server/src/test/resources/fitbit-activity.json index 611ce21d..93fe5b5f 100644 --- a/shim-server/src/test/resources/fitbit-activity.json +++ b/shim-server/src/test/resources/fitbit-activity.json @@ -1,172 +1,80 @@ -{"shim": null, "timeStamp": 1408647736, "body": [ - { - "result": { - "date": "2014-08-19", - "content": { - "activities": [], - "summary": { - "activeScore": -1, - "activityCalories": 0, - "caloriesBMR": 1286, - "caloriesOut": 1286, - "distances": [ - { - "activity": "total", - "distance": 0 - }, - { - "activity": "tracker", - "distance": 0 - }, - { - "activity": "loggedActivities", - "distance": 0 - }, - { - "activity": "veryActive", - "distance": 0 - }, - { - "activity": "moderatelyActive", - "distance": 0 - }, - { - "activity": "lightlyActive", - "distance": 0 - }, - { - "activity": "sedentaryActive", - "distance": 0 - } - ], - "elevation": 0, - "fairlyActiveMinutes": 0, - "floors": 0, - "lightlyActiveMinutes": 0, - "marginalCalories": 0, - "sedentaryMinutes": 1440, - "steps": 0, - "veryActiveMinutes": 0 - } - } - } - }, - { - "result": { - "date": "2014-08-20", - "content": { - "activities": [], - "goals": { - "activeMinutes": 30, - "caloriesOut": 2175, - "distance": 8.05, - "floors": 10, - "steps": 10000 - }, - "summary": { - "activeScore": -1, - "activityCalories": 628, - "caloriesBMR": 1286, - "caloriesOut": 1683, - "distances": [ - { - "activity": "total", - "distance": 3.05 - }, - { - "activity": "tracker", - "distance": 3.05 - }, - { - "activity": "loggedActivities", - "distance": 0 - }, - { - "activity": "veryActive", - "distance": 0 - }, - { - "activity": "moderatelyActive", - "distance": 1.2 - }, - { - "activity": "lightlyActive", - "distance": 1.83 - }, - { - "activity": "sedentaryActive", - "distance": 0.01 - } - ], - "elevation": 48.77, - "fairlyActiveMinutes": 41, - "floors": 16, - "lightlyActiveMinutes": 215, - "marginalCalories": 344, - "sedentaryMinutes": 607, - "steps": 4332, - "veryActiveMinutes": 0 - } - } - } - }, - { - "result": { - "date": "2014-08-21", - "content": { - "activities": [], - "goals": { - "activeMinutes": 30, - "caloriesOut": 2175, - "distance": 8.05, - "floors": 10, - "steps": 10000 - }, - "summary": { - "activeScore": -1, - "activityCalories": 265, - "caloriesBMR": 751, - "caloriesOut": 917, - "distances": [ - { - "activity": "total", - "distance": 1.37 - }, - { - "activity": "tracker", - "distance": 1.37 - }, - { - "activity": "loggedActivities", - "distance": 0 - }, - { - "activity": "veryActive", - "distance": 0 - }, - { - "activity": "moderatelyActive", - "distance": 0.31 - }, - { - "activity": "lightlyActive", - "distance": 1.05 - }, - { - "activity": "sedentaryActive", - "distance": 0.01 - } - ], - "elevation": 30.48, - "fairlyActiveMinutes": 12, - "floors": 10, - "lightlyActiveMinutes": 99, - "marginalCalories": 142, - "sedentaryMinutes": 457, - "steps": 1944, - "veryActiveMinutes": 0 - } - } - } - } -]} \ No newline at end of file +{ + "result": { + "date": "2015-04-28", + "content": { + "activities": [ + { + "activityId": 58001, + "activityParentId": 90024, + "activityParentName": "Swimming", + "calories": 141, + "description": "less than 25 yards/min", + "distance": 0.48, + "duration": 1800000, + "hasStartTime": true, + "isFavorite": false, + "lastModified": "2015-04-30T00:45:22.290-07:00", + "logId": 207756316, + "name": "Swimming", + "startDate": "2015-04-28", + "startTime": "21:30" + } + ], + "goals": { + "activeMinutes": 30, + "caloriesOut": 0, + "distance": 8.05, + "floors": 30, + "steps": 10000 + }, + "summary": { + "activeScore": -1, + "activityCalories": 1024, + "caloriesBMR": 1688, + "caloriesOut": 2484, + "distances": [ + { + "activity": "total", + "distance": 5.8 + }, + { + "activity": "tracker", + "distance": 5.8 + }, + { + "activity": "loggedActivities", + "distance": 0.48 + }, + { + "activity": "veryActive", + "distance": 4.01 + }, + { + "activity": "moderatelyActive", + "distance": 0.3 + }, + { + "activity": "lightlyActive", + "distance": 1.49 + }, + { + "activity": "sedentaryActive", + "distance": 0 + }, + { + "activity": "Swimming", + "distance": 0.48 + } + ], + "elevation": 85.34, + "fairlyActiveMinutes": 37, + "floors": 28, + "lightlyActiveMinutes": 109, + "marginalCalories": 649, + "sedentaryMinutes": 740, + "steps": 7551, + "veryActiveMinutes": 48 + } + } + } +} diff --git a/shim-server/src/test/resources/fitbit-bp.json b/shim-server/src/test/resources/fitbit-bp.json index 773d5fdb..e45a7585 100644 --- a/shim-server/src/test/resources/fitbit-bp.json +++ b/shim-server/src/test/resources/fitbit-bp.json @@ -1,27 +1,32 @@ -{"shim": null, "timeStamp": 1407814591, "body": { - "average": { - "condition": "Prehypertension", - "diastolic": 79, - "systolic": 124 - }, - "bp": [ - { - "diastolic": 82, - "logId": 437175259, - "systolic": 131, - "time": "11:30" - }, - { - "diastolic": 75, - "logId": 437206449, - "systolic": 122, - "time": "12:30" - }, - { - "diastolic": 80, - "logId": 437214560, - "systolic": 120, - "time": "13:30" - } - ] -}} \ No newline at end of file +{ + "result": { + "date": "2014-07-13", + "content": { + "average": { + "condition": "Prehypertension", + "diastolic": 79, + "systolic": 124 + }, + "bp": [ + { + "diastolic": 82, + "logId": 437175259, + "systolic": 131, + "time": "11:30" + }, + { + "diastolic": 75, + "logId": 437206449, + "systolic": 122, + "time": "12:30" + }, + { + "diastolic": 80, + "logId": 437214560, + "systolic": 120, + "time": "13:30" + } + ] + } + } +} diff --git a/shim-server/src/test/resources/fitbit-glucose.json b/shim-server/src/test/resources/fitbit-glucose.json index 0a74d820..7fa6376a 100644 --- a/shim-server/src/test/resources/fitbit-glucose.json +++ b/shim-server/src/test/resources/fitbit-glucose.json @@ -1,19 +1,24 @@ -{"shim": null, "timeStamp": 1407814448, "body": { - "glucose": [ - { - "glucose": 5, - "tracker": "Morning", - "time": "12:30" - }, - { - "glucose": 5.1, - "tracker": "Afternoon", - "time": "12:30" - }, - { - "glucose": 5.5, - "tracker": "Evening", - "time": "12:30" - } - ] -}} \ No newline at end of file +{ + "result": { + "date": "2014-07-13", + "content": { + "glucose": [ + { + "glucose": 5, + "tracker": "Morning", + "time": "12:30" + }, + { + "glucose": 5.1, + "tracker": "Afternoon", + "time": "12:30" + }, + { + "glucose": 5.5, + "tracker": "Evening", + "time": "12:30" + } + ] + } + } +} diff --git a/shim-server/src/test/resources/fitbit-activities-1m.json b/shim-server/src/test/resources/fitbit-step-count-1m.json similarity index 100% rename from shim-server/src/test/resources/fitbit-activities-1m.json rename to shim-server/src/test/resources/fitbit-step-count-1m.json diff --git a/shim-server/src/test/resources/fitbit-step-count.json b/shim-server/src/test/resources/fitbit-step-count.json new file mode 100644 index 00000000..7dcf114c --- /dev/null +++ b/shim-server/src/test/resources/fitbit-step-count.json @@ -0,0 +1,15 @@ +[ + { + "result": { + "date": "2014-08-20", + "content": { + "activities-steps": [ + { + "dateTime": "2014-08-20", + "value": "4332" + } + ] + } + } + } +] diff --git a/shim-server/src/test/resources/fitbit-weight-multi.json b/shim-server/src/test/resources/fitbit-weight-multi.json deleted file mode 100644 index 4deaf137..00000000 --- a/shim-server/src/test/resources/fitbit-weight-multi.json +++ /dev/null @@ -1,23 +0,0 @@ -{"weight": [ - { - "bmi": 17.07, - "date": "2014-11-12", - "logId": 1415836799000, - "time": "23:59:59", - "weight": 49.4 - }, - { - "bmi": 18.01, - "date": "2014-11-13", - "logId": 1415923199000, - "time": "23:59:59", - "weight": 52.1 - }, - { - "bmi": 17.54, - "date": "2014-11-14", - "logId": 1416009599000, - "time": "23:59:59", - "weight": 50.8 - } -]} \ No newline at end of file diff --git a/shim-server/src/test/resources/fitbit-weight.json b/shim-server/src/test/resources/fitbit-weight.json index 7cd0f032..c58b54be 100644 --- a/shim-server/src/test/resources/fitbit-weight.json +++ b/shim-server/src/test/resources/fitbit-weight.json @@ -1,4 +1,4 @@ -{"shim": null, "timeStamp": 1407814624, "body": { +{ "weight": [ { "bmi": 17.07, @@ -22,4 +22,4 @@ "weight": 50.8 } ] -}} \ No newline at end of file +} diff --git a/shim-server/src/test/resources/ihealth-activity.json b/shim-server/src/test/resources/ihealth-activity.json new file mode 100644 index 00000000..35c651d4 --- /dev/null +++ b/shim-server/src/test/resources/ihealth-activity.json @@ -0,0 +1,20 @@ +{ + "SPORTDataList": [ + { + "SportName": "Run ", + "SportStartTime": 1418292000, + "SportEndTime": 1418295600, + "TimeZone": -1, + "Lat": -1, + "Lon": -1, + "DataID": "90ab964ced9d46b789d9ab84d51beb4d", + "Calories": 1000.1 + } + ], + "CurrentRecordCount": 1, + "NextPageUrl": "", + "PageLength": 50, + "PageNumber": 1, + "PrevPageUrl": "", + "RecordCount": 1 +} diff --git a/shim-server/src/test/resources/ihealth-blood-glucose.json b/shim-server/src/test/resources/ihealth-blood-glucose.json new file mode 100644 index 00000000..c26ec0e9 --- /dev/null +++ b/shim-server/src/test/resources/ihealth-blood-glucose.json @@ -0,0 +1,23 @@ +{ + "BGDataList": [ + { + "BG": 80.5, + "DataID": "90ab964ced9d46b789d9ab84d51beb4d", + "DataSource": "Manual", + "DinnerSituation": "Before_lunch", + "DrugSituation": "Before_taking_pills", + "LastChangeTime": 1418329906, + "Lat": 47.6097, + "Lon": -122.3331, + "MDate": 1418301049, + "Note": "testing" + } + ], + "BGUnit": 0, + "CurrentRecordCount": 1, + "NextPageUrl": "", + "PageLength": 50, + "PageNumber": 1, + "PrevPageUrl": "", + "RecordCount": 1 +} diff --git a/shim-server/src/test/resources/ihealth-blood-pressure.json b/shim-server/src/test/resources/ihealth-blood-pressure.json new file mode 100644 index 00000000..de61e6f4 --- /dev/null +++ b/shim-server/src/test/resources/ihealth-blood-pressure.json @@ -0,0 +1,25 @@ +{ + "BPDataList": [ + { + "BPL": 6, + "DataID": "88bad68878044d079294470c9ffff8aa", + "DataSource": "Manual", + "HP": 100, + "HR": 60, + "IsArr": -1, + "LP": 50, + "LastChangeTime": 1418316989, + "Lat": -1, + "Lon": -1, + "MDate": 1418288156, + "Note": "testing" + } + ], + "BPUnit": 0, + "CurrentRecordCount": 1, + "NextPageUrl": "", + "PageLength": 50, + "PageNumber": 1, + "PrevPageUrl": "", + "RecordCount": 1 +} diff --git a/shim-server/src/test/resources/ihealth-body-weight.json b/shim-server/src/test/resources/ihealth-body-weight.json new file mode 100644 index 00000000..fe574f2a --- /dev/null +++ b/shim-server/src/test/resources/ihealth-body-weight.json @@ -0,0 +1,39 @@ +{ + "CurrentRecordCount": 2, + "NextPageUrl": "", + "PageLength": 50, + "PageNumber": 1, + "PrevPageUrl": "", + "RecordCount": 2, + "WeightDataList": [ + { + "BMI": 21.13917370644493, + "BoneValue": 0, + "DCI": 1884.3092505737952, + "DataID": "00696c63ca784a4d9c1809a526e23d71", + "DataSource": "Manual", + "FatValue": 11, + "LastChangeTime": 1418315034, + "MDate": 1418197090, + "MuscaleValue": 0, + "Note": "", + "WaterValue": 0, + "WeightValue": 75.86718536514545 + }, + { + "BMI": 20.989716993834165, + "BoneValue": 0, + "DCI": 1859.0779363336994, + "DataID": "38b0ac53a4314cc88566d2097de21b68", + "DataSource": "Manual", + "FatValue": 12, + "LastChangeTime": 1418314989, + "MDate": 1418282981, + "MuscaleValue": 0, + "Note": "testing", + "WaterValue": 0, + "WeightValue": 74.76518405458972 + } + ], + "WeightUnit": 1 +} diff --git a/shim-server/src/test/resources/ihealth-sleep.json b/shim-server/src/test/resources/ihealth-sleep.json new file mode 100644 index 00000000..bd31d23d --- /dev/null +++ b/shim-server/src/test/resources/ihealth-sleep.json @@ -0,0 +1,23 @@ +{ + "SRDataList": [ + { + "Awaken": 1, + "DataID": "90ab964ced9d46b789d9ab84d51beb4d", + "Fallsleep": "?", + "HoursSlept": 7.5, + "SleepEfficiency": 80, + "Lat": -1, + "Lon": -1, + "StartTime": 1418252400, + "EndTime": 1418281200, + "Note": "zzz" + } + ], + "FoodUnit": 1, + "CurrentRecordCount": 1, + "NextPageUrl": "", + "PageLength": 50, + "PageNumber": 1, + "PrevPageUrl": "", + "RecordCount": 1 +} diff --git a/shim-server/src/test/resources/ihealth-step-count.json b/shim-server/src/test/resources/ihealth-step-count.json new file mode 100644 index 00000000..9e412f89 --- /dev/null +++ b/shim-server/src/test/resources/ihealth-step-count.json @@ -0,0 +1,22 @@ +{ + "ARDataList": [ + { + "Calories": 1000.1, + "DataID": "90ab964ced9d46b789d9ab84d51beb4d", + "DistanceTraveled": 5.5, + "SleepEfficiency": 80, + "Lat": -1, + "Lon": -1, + "Steps": 9000, + "MDate": 1418281200, + "Note": "" + } + ], + "DistanceUnit": 1, + "CurrentRecordCount": 1, + "NextPageUrl": "", + "PageLength": 50, + "PageNumber": 1, + "PrevPageUrl": "", + "RecordCount": 1 +} diff --git a/shim-server/src/test/resources/jawbone-body-weight.json b/shim-server/src/test/resources/jawbone-body-weight.json new file mode 100644 index 00000000..fadabab2 --- /dev/null +++ b/shim-server/src/test/resources/jawbone-body-weight.json @@ -0,0 +1,51 @@ +{ + "meta": { + "user_xid": "T-v4-jQoO5pN1eqBXkrLkQ", + "message": "OK", + "code": 200, + "time": 1429209473 + }, + "data": { + "items": [ + { + "lean_mass": null, + "time_updated": 1415212004, + "xid": "W7C8ClyS84KPZKT45hK4G3xnGDMYryW4", + "weight": 70.76041, + "title": null, + "body_fat": 12, + "image": "", + "bmi": null, + "time_created": 1415206811, + "note": null, + "details": { + "tz": "America/Los_Angeles" + }, + "waistline": null, + "date": 20141105, + "shared": false, + "type": "body" + }, + { + "lean_mass": null, + "time_updated": 1415212401, + "xid": "W7C8ClyS84LGuL9INlXL0-Nb0YifWU-R", + "weight": 70.7604, + "title": null, + "body_fat": 12, + "image": "", + "bmi": null, + "time_created": 1415212364, + "note": null, + "details": { + "tz": "America/Los_Angeles" + }, + "waistline": null, + "date": 20141105, + "shared": false, + "type": "body" + } + ], + "size": 2 + } +} diff --git a/shim-server/src/test/resources/jawbone-sleep.json b/shim-server/src/test/resources/jawbone-sleep.json new file mode 100644 index 00000000..7f872e0d --- /dev/null +++ b/shim-server/src/test/resources/jawbone-sleep.json @@ -0,0 +1,44 @@ +{ + "meta": { + "user_xid": "T-v4-jQoO5pN1eqBXkrLkQ", + "message": "OK", + "code": 200, + "time": 1429210224 + }, + "data": { + "items": [ + { + "time_updated": 1409341770, + "xid": "7bmIm0ICa93CjNwY3D2N0g", + "title": "for 6h 12m", + "time_created": 1393995600, + "time_completed": 1394026045, + "details": { + "body": 0, + "sound": 14203, + "smart_alarm_fire": 0, + "awakenings": 0, + "light": 8126, + "mind": 0, + "asleep_time": 1393995600, + "awake_time": 1394026045, + "awake": 8116, + "rem": 0, + "duration": 30445, + "tz": "America/Los_Angeles", + "quality": 0, + "sunset": null, + "sunrise": null + }, + "date": 20140305, + "shared": true, + "snapshot_image": "/nudge/image/e/1409341770/7bmIm0ICa93CjNwY3D2N0g/fqFmGkTSvSw.png", + "sub_type": 0 + } + ], + "links": { + "next": "/nudge/api/v.1.1/users/T-v4-jQoO5pN1eqBXkrLkQ/sleeps?start_time=1397939921&updated_after=0&limit=50&end_time=1429167600" + }, + "size": 50 + } +} diff --git a/shim-server/src/test/resources/jawbone-workouts.json b/shim-server/src/test/resources/jawbone-workouts.json deleted file mode 100644 index 7b28c2d3..00000000 --- a/shim-server/src/test/resources/jawbone-workouts.json +++ /dev/null @@ -1,94 +0,0 @@ -{"shim": null, "timeStamp": 1408407796, "body": { - "meta": { - "user_xid": "gG0aWvo9bXwLSMyOos3njA", - "message": "OK", - "code": 200, - "time": 1408407796 - }, - "data": { - "items": [ - { - "reaction": null, - "time_completed": 1408376384, - "xid": "UDZ763h_uKw-OTw34D0Chw", - "title": "Walk", - "image": "", - "time_created": 1408376384, - "time_updated": 1408376463, - "details": { - "tz": "America/Los_Angeles", - "goal": 0, - "calories": 1500.0, - "place_acc": 0, - "bmr": 0, - "intensity": 1, - "bg_calories": 0, - "meters": 2500, - "km": 2.5, - "time": 0, - "bg_active_time": 0, - "steps": 0, - "bmr_calories": 0.0 - }, - "date": 20140818, - "snapshot_image": "", - "sub_type": 1 - }, - { - "reaction": null, - "time_completed": 1408376477, - "xid": "UDZ763h_uKzz-MZA44Fzng", - "title": "Swim", - "image": "", - "time_created": 1408372477, - "time_updated": 1408376587, - "details": { - "tz": "America/Los_Angeles", - "goal": 0, - "calories": 3000.0, - "place_acc": 0, - "bmr": 0, - "intensity": 3, - "bg_calories": 0, - "meters": 25, - "km": 0.025, - "time": 4000, - "bg_active_time": 0, - "steps": 0, - "bmr_calories": 0.0 - }, - "date": 20140818, - "snapshot_image": "", - "sub_type": 13 - }, - { - "reaction": null, - "time_completed": 1408407698, - "xid": "UDZ763h_uKw7hqpx_ZoBhA", - "title": "Walk", - "image": "", - "time_created": 1408407698, - "time_updated": 1408407779, - "details": { - "tz": "America/Los_Angeles", - "goal": 0, - "calories": 600.0, - "place_acc": 0, - "bmr": 0, - "intensity": 1, - "bg_calories": 0, - "meters": 1000, - "km": 1.0, - "time": 0, - "bg_active_time": 0, - "steps": 0, - "bmr_calories": 0.0 - }, - "date": 20140818, - "snapshot_image": "", - "sub_type": 1 - } - ], - "size": 3 - } -}} \ No newline at end of file diff --git a/shim-server/src/test/resources/logback-test.xml b/shim-server/src/test/resources/logback-test.xml new file mode 100644 index 00000000..477096c9 --- /dev/null +++ b/shim-server/src/test/resources/logback-test.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/shim-server/src/test/resources/misfit-activities.json b/shim-server/src/test/resources/misfit-activities.json new file mode 100644 index 00000000..38536f8c --- /dev/null +++ b/shim-server/src/test/resources/misfit-activities.json @@ -0,0 +1,34 @@ +{ + "sessions": [ + { + "id": "552eab896c59ae1f7300003e", + "activityType": "Walking", + "startTime": "2015-04-13T11:46:00-07:00", + "duration": 1140, + "points": 228.8, + "steps": 2026, + "calories": 96.8, + "distance": 0.9371 + }, + { + "id": "552edc21472c922550000027", + "activityType": "Walking", + "startTime": "2015-04-15T13:27:00-07:00", + "duration": 420, + "points": 0.8, + "steps": 8, + "calories": 0.3, + "distance": 0.003 + }, + { + "id": "552edc21472c922550000028", + "activityType": "Walking", + "startTime": "2015-04-15T14:08:00-07:00", + "duration": 2340, + "points": 25.6, + "steps": 256, + "calories": 10.8, + "distance": 0.0969 + } + ] +} diff --git a/shim-server/src/test/resources/org/openmhealth/shim/fitbit/mapper/fitbit-get-activities-empty.json b/shim-server/src/test/resources/org/openmhealth/shim/fitbit/mapper/fitbit-get-activities-empty.json new file mode 100644 index 00000000..40cf9bd3 --- /dev/null +++ b/shim-server/src/test/resources/org/openmhealth/shim/fitbit/mapper/fitbit-get-activities-empty.json @@ -0,0 +1,51 @@ +{ + "activities": [], + "goals": { + "activeMinutes": 30, + "caloriesOut": 2095, + "distance": 8.05, + "steps": 10000 + }, + "summary": { + "activeScore": -1, + "activityCalories": 799, + "caloriesBMR": 1240, + "caloriesOut": 1892, + "distances": [ + { + "activity": "total", + "distance": 8.56 + }, + { + "activity": "tracker", + "distance": 8.56 + }, + { + "activity": "loggedActivities", + "distance": 0 + }, + { + "activity": "veryActive", + "distance": 3.31 + }, + { + "activity": "moderatelyActive", + "distance": 1.15 + }, + { + "activity": "lightlyActive", + "distance": 4.1 + }, + { + "activity": "sedentaryActive", + "distance": 0 + } + ], + "fairlyActiveMinutes": 26, + "lightlyActiveMinutes": 112, + "marginalCalories": 522, + "sedentaryMinutes": 1260, + "steps": 12517, + "veryActiveMinutes": 42 + } +} \ No newline at end of file diff --git a/shim-server/src/test/resources/org/openmhealth/shim/fitbit/mapper/fitbit-get-activities-multiple.json b/shim-server/src/test/resources/org/openmhealth/shim/fitbit/mapper/fitbit-get-activities-multiple.json new file mode 100644 index 00000000..3c7a680a --- /dev/null +++ b/shim-server/src/test/resources/org/openmhealth/shim/fitbit/mapper/fitbit-get-activities-multiple.json @@ -0,0 +1,113 @@ +{ + "activities": [ + { + "activityId": 12120, + "activityParentId": 90009, + "activityParentName": "Run", + "calories": 150, + "description": "10 mph (6 min/mile)", + "distance": 6.43738, + "duration": 1440000, + "hasStartTime": true, + "isFavorite": false, + "lastModified": "2015-06-23T21:41:37.000Z", + "logId": 253202765, + "name": "Run", + "startDate": "2015-06-23", + "startTime": "11:55", + "steps": 6761 + }, + { + "activityId": 58002, + "activityParentId": 90024, + "activityParentName": "Swimming", + "calories": 100, + "description": "50-75 yards/min", + "hasStartTime": true, + "isFavorite": false, + "lastModified": "2015-06-23T21:41:04.000Z", + "logId": 253246706, + "name": "Swimming", + "startDate": "2015-06-23", + "startTime": "10:00", + "steps": 0 + }, + { + "activityId": 17200, + "activityParentId": 90013, + "activityParentName": "Walk", + "calories": 200, + "description": "3.5 mph, walking for exercise", + "duration": 3600000, + "distance": 6.43738, + "hasStartTime": false, + "isFavorite": false, + "lastModified": "2015-06-23T21:42:44.000Z", + "logId": 253202766, + "name": "Walk", + "startDate": "2015-06-23", + "startTime": "15:00", + "steps": 8481 + } + ], + "goals": { + "activeMinutes": 30, + "caloriesOut": 2859, + "distance": 8.05, + "steps": 10000 + }, + "summary": { + "activeScore": -1, + "activityCalories": 150, + "caloriesBMR": 1069, + "caloriesOut": 1218, + "distances": [ + { + "activity": "Run", + "distance": 6.43738 + }, + { + "activity": "Swimming", + "distance": 3.21869 + }, + { + "activity": "Walk", + "distance": 6.43738 + }, + { + "activity": "total", + "distance": 12.87 + }, + { + "activity": "tracker", + "distance": 0 + }, + { + "activity": "loggedActivities", + "distance": 16.09345 + }, + { + "activity": "veryActive", + "distance": 6.44 + }, + { + "activity": "moderatelyActive", + "distance": 0 + }, + { + "activity": "lightlyActive", + "distance": 6.44 + }, + { + "activity": "sedentaryActive", + "distance": 0 + } + ], + "fairlyActiveMinutes": 0, + "lightlyActiveMinutes": 72, + "marginalCalories": 96, + "sedentaryMinutes": 787, + "steps": 15242, + "veryActiveMinutes": 24 + } +} \ No newline at end of file diff --git a/shim-server/src/test/resources/org/openmhealth/shim/fitbit/mapper/fitbit-get-activities-single.json b/shim-server/src/test/resources/org/openmhealth/shim/fitbit/mapper/fitbit-get-activities-single.json new file mode 100644 index 00000000..4429eac3 --- /dev/null +++ b/shim-server/src/test/resources/org/openmhealth/shim/fitbit/mapper/fitbit-get-activities-single.json @@ -0,0 +1,67 @@ +{ + "activities": [ + { + "activityId": 17152, + "activityParentId": 90013, + "activityParentName": "Walk", + "calories": 128, + "description": "2.0 mph, slow pace", + "distance": 3.36, + "duration": 3600000, + "hasStartTime": true, + "isFavorite": false, + "lastModified": "2014-06-19T18:52:32.000Z", + "logId": 79441095, + "name": "Walk", + "startDate": "2014-06-19", + "startTime": "09:00", + "steps": 5000 + } + ], + "summary": { + "activeScore": -1, + "activityCalories": 420, + "caloriesBMR": 1221, + "caloriesOut": 1848, + "distances": [ + { + "activity": "Walk", + "distance": 3.36 + }, + { + "activity": "total", + "distance": 7.32 + }, + { + "activity": "tracker", + "distance": 7.32 + }, + { + "activity": "loggedActivities", + "distance": 3.36 + }, + { + "activity": "veryActive", + "distance": 0 + }, + { + "activity": "moderatelyActive", + "distance": 0 + }, + { + "activity": "lightlyActive", + "distance": 5.23 + }, + { + "activity": "sedentaryActive", + "distance": 0 + } + ], + "fairlyActiveMinutes": 34, + "lightlyActiveMinutes": 132, + "marginalCalories": 184, + "sedentaryMinutes": 1271, + "steps": 10007, + "veryActiveMinutes": 3 + } +} \ No newline at end of file diff --git a/shim-server/src/test/resources/org/openmhealth/shim/fitbit/mapper/fitbit-get-body-weight.json b/shim-server/src/test/resources/org/openmhealth/shim/fitbit/mapper/fitbit-get-body-weight.json new file mode 100644 index 00000000..24b6cf7d --- /dev/null +++ b/shim-server/src/test/resources/org/openmhealth/shim/fitbit/mapper/fitbit-get-body-weight.json @@ -0,0 +1,32 @@ +{ + "weight": [ + { + "bmi": 21.48, + "date": "2015-05-13", + "logId": 1431541739000, + "time": "18:28:59", + "weight": 56.7 + }, + { + "bmi": 21.17, + "date": "2015-05-14", + "logId": 1431604317000, + "time": "11:51:57", + "weight": 55.9 + }, + { + "bmi": 21.99, + "date": "2015-05-22", + "logId": 1432318326000, + "time": "18:12:06", + "weight": 58.1 + }, + { + "bmi": 21.65, + "date": "2015-05-24", + "logId": 1432480525000, + "time": "15:15:25", + "weight": 57.2 + } + ] +} \ No newline at end of file diff --git a/shim-server/src/test/resources/org/openmhealth/shim/fitbit/mapper/fitbit-get-sleep-empty.json b/shim-server/src/test/resources/org/openmhealth/shim/fitbit/mapper/fitbit-get-sleep-empty.json new file mode 100644 index 00000000..a02ae4a2 --- /dev/null +++ b/shim-server/src/test/resources/org/openmhealth/shim/fitbit/mapper/fitbit-get-sleep-empty.json @@ -0,0 +1,8 @@ +{ + "sleep": [], + "summary": { + "totalMinutesAsleep": 0, + "totalSleepRecords": 0, + "totalTimeInBed": 0 + } +} \ No newline at end of file diff --git a/shim-server/src/test/resources/org/openmhealth/shim/fitbit/mapper/fitbit-get-sleep-multiple.json b/shim-server/src/test/resources/org/openmhealth/shim/fitbit/mapper/fitbit-get-sleep-multiple.json new file mode 100644 index 00000000..d7ed9f73 --- /dev/null +++ b/shim-server/src/test/resources/org/openmhealth/shim/fitbit/mapper/fitbit-get-sleep-multiple.json @@ -0,0 +1,2329 @@ +{ + "sleep": [ + { + "awakeCount": 0, + "awakeDuration": 0, + "awakeningsCount": 0, + "dateOfSleep": "2015-06-08", + "duration": 5400000, + "efficiency": 100, + "isMainSleep": false, + "logId": 1008298328, + "minuteData": [ + { + "dateTime": "13:00:00", + "value": "1" + }, + { + "dateTime": "13:01:00", + "value": "1" + }, + { + "dateTime": "13:02:00", + "value": "1" + }, + { + "dateTime": "13:03:00", + "value": "1" + }, + { + "dateTime": "13:04:00", + "value": "1" + }, + { + "dateTime": "13:05:00", + "value": "1" + }, + { + "dateTime": "13:06:00", + "value": "1" + }, + { + "dateTime": "13:07:00", + "value": "1" + }, + { + "dateTime": "13:08:00", + "value": "1" + }, + { + "dateTime": "13:09:00", + "value": "1" + }, + { + "dateTime": "13:10:00", + "value": "1" + }, + { + "dateTime": "13:11:00", + "value": "1" + }, + { + "dateTime": "13:12:00", + "value": "1" + }, + { + "dateTime": "13:13:00", + "value": "1" + }, + { + "dateTime": "13:14:00", + "value": "1" + }, + { + "dateTime": "13:15:00", + "value": "1" + }, + { + "dateTime": "13:16:00", + "value": "1" + }, + { + "dateTime": "13:17:00", + "value": "1" + }, + { + "dateTime": "13:18:00", + "value": "1" + }, + { + "dateTime": "13:19:00", + "value": "1" + }, + { + "dateTime": "13:20:00", + "value": "1" + }, + { + "dateTime": "13:21:00", + "value": "1" + }, + { + "dateTime": "13:22:00", + "value": "1" + }, + { + "dateTime": "13:23:00", + "value": "1" + }, + { + "dateTime": "13:24:00", + "value": "1" + }, + { + "dateTime": "13:25:00", + "value": "1" + }, + { + "dateTime": "13:26:00", + "value": "1" + }, + { + "dateTime": "13:27:00", + "value": "1" + }, + { + "dateTime": "13:28:00", + "value": "1" + }, + { + "dateTime": "13:29:00", + "value": "1" + }, + { + "dateTime": "13:30:00", + "value": "1" + }, + { + "dateTime": "13:31:00", + "value": "1" + }, + { + "dateTime": "13:32:00", + "value": "1" + }, + { + "dateTime": "13:33:00", + "value": "1" + }, + { + "dateTime": "13:34:00", + "value": "1" + }, + { + "dateTime": "13:35:00", + "value": "1" + }, + { + "dateTime": "13:36:00", + "value": "1" + }, + { + "dateTime": "13:37:00", + "value": "1" + }, + { + "dateTime": "13:38:00", + "value": "1" + }, + { + "dateTime": "13:39:00", + "value": "1" + }, + { + "dateTime": "13:40:00", + "value": "1" + }, + { + "dateTime": "13:41:00", + "value": "1" + }, + { + "dateTime": "13:42:00", + "value": "1" + }, + { + "dateTime": "13:43:00", + "value": "1" + }, + { + "dateTime": "13:44:00", + "value": "1" + }, + { + "dateTime": "13:45:00", + "value": "1" + }, + { + "dateTime": "13:46:00", + "value": "1" + }, + { + "dateTime": "13:47:00", + "value": "1" + }, + { + "dateTime": "13:48:00", + "value": "1" + }, + { + "dateTime": "13:49:00", + "value": "1" + }, + { + "dateTime": "13:50:00", + "value": "1" + }, + { + "dateTime": "13:51:00", + "value": "1" + }, + { + "dateTime": "13:52:00", + "value": "1" + }, + { + "dateTime": "13:53:00", + "value": "1" + }, + { + "dateTime": "13:54:00", + "value": "1" + }, + { + "dateTime": "13:55:00", + "value": "1" + }, + { + "dateTime": "13:56:00", + "value": "1" + }, + { + "dateTime": "13:57:00", + "value": "1" + }, + { + "dateTime": "13:58:00", + "value": "1" + }, + { + "dateTime": "13:59:00", + "value": "1" + }, + { + "dateTime": "14:00:00", + "value": "1" + }, + { + "dateTime": "14:01:00", + "value": "1" + }, + { + "dateTime": "14:02:00", + "value": "1" + }, + { + "dateTime": "14:03:00", + "value": "1" + }, + { + "dateTime": "14:04:00", + "value": "1" + }, + { + "dateTime": "14:05:00", + "value": "1" + }, + { + "dateTime": "14:06:00", + "value": "1" + }, + { + "dateTime": "14:07:00", + "value": "1" + }, + { + "dateTime": "14:08:00", + "value": "1" + }, + { + "dateTime": "14:09:00", + "value": "1" + }, + { + "dateTime": "14:10:00", + "value": "1" + }, + { + "dateTime": "14:11:00", + "value": "1" + }, + { + "dateTime": "14:12:00", + "value": "1" + }, + { + "dateTime": "14:13:00", + "value": "1" + }, + { + "dateTime": "14:14:00", + "value": "1" + }, + { + "dateTime": "14:15:00", + "value": "1" + }, + { + "dateTime": "14:16:00", + "value": "1" + }, + { + "dateTime": "14:17:00", + "value": "1" + }, + { + "dateTime": "14:18:00", + "value": "1" + }, + { + "dateTime": "14:19:00", + "value": "1" + }, + { + "dateTime": "14:20:00", + "value": "1" + }, + { + "dateTime": "14:21:00", + "value": "1" + }, + { + "dateTime": "14:22:00", + "value": "1" + }, + { + "dateTime": "14:23:00", + "value": "1" + }, + { + "dateTime": "14:24:00", + "value": "1" + }, + { + "dateTime": "14:25:00", + "value": "1" + }, + { + "dateTime": "14:26:00", + "value": "1" + }, + { + "dateTime": "14:27:00", + "value": "1" + }, + { + "dateTime": "14:28:00", + "value": "1" + }, + { + "dateTime": "14:29:00", + "value": "1" + } + ], + "minutesAfterWakeup": 0, + "minutesAsleep": 90, + "minutesAwake": 0, + "minutesToFallAsleep": 0, + "restlessCount": 0, + "restlessDuration": 0, + "startTime": "2015-06-08T13:00:00.000", + "timeInBed": 90 + }, + { + "awakeCount": 0, + "awakeDuration": 0, + "awakeningsCount": 0, + "dateOfSleep": "2015-06-08", + "duration": 28800000, + "efficiency": 100, + "isMainSleep": true, + "logId": 1008807164, + "minuteData": [ + { + "dateTime": "22:00:00", + "value": "1" + }, + { + "dateTime": "22:01:00", + "value": "1" + }, + { + "dateTime": "22:02:00", + "value": "1" + }, + { + "dateTime": "22:03:00", + "value": "1" + }, + { + "dateTime": "22:04:00", + "value": "1" + }, + { + "dateTime": "22:05:00", + "value": "1" + }, + { + "dateTime": "22:06:00", + "value": "1" + }, + { + "dateTime": "22:07:00", + "value": "1" + }, + { + "dateTime": "22:08:00", + "value": "1" + }, + { + "dateTime": "22:09:00", + "value": "1" + }, + { + "dateTime": "22:10:00", + "value": "1" + }, + { + "dateTime": "22:11:00", + "value": "1" + }, + { + "dateTime": "22:12:00", + "value": "1" + }, + { + "dateTime": "22:13:00", + "value": "1" + }, + { + "dateTime": "22:14:00", + "value": "1" + }, + { + "dateTime": "22:15:00", + "value": "1" + }, + { + "dateTime": "22:16:00", + "value": "1" + }, + { + "dateTime": "22:17:00", + "value": "1" + }, + { + "dateTime": "22:18:00", + "value": "1" + }, + { + "dateTime": "22:19:00", + "value": "1" + }, + { + "dateTime": "22:20:00", + "value": "1" + }, + { + "dateTime": "22:21:00", + "value": "1" + }, + { + "dateTime": "22:22:00", + "value": "1" + }, + { + "dateTime": "22:23:00", + "value": "1" + }, + { + "dateTime": "22:24:00", + "value": "1" + }, + { + "dateTime": "22:25:00", + "value": "1" + }, + { + "dateTime": "22:26:00", + "value": "1" + }, + { + "dateTime": "22:27:00", + "value": "1" + }, + { + "dateTime": "22:28:00", + "value": "1" + }, + { + "dateTime": "22:29:00", + "value": "1" + }, + { + "dateTime": "22:30:00", + "value": "1" + }, + { + "dateTime": "22:31:00", + "value": "1" + }, + { + "dateTime": "22:32:00", + "value": "1" + }, + { + "dateTime": "22:33:00", + "value": "1" + }, + { + "dateTime": "22:34:00", + "value": "1" + }, + { + "dateTime": "22:35:00", + "value": "1" + }, + { + "dateTime": "22:36:00", + "value": "1" + }, + { + "dateTime": "22:37:00", + "value": "1" + }, + { + "dateTime": "22:38:00", + "value": "1" + }, + { + "dateTime": "22:39:00", + "value": "1" + }, + { + "dateTime": "22:40:00", + "value": "1" + }, + { + "dateTime": "22:41:00", + "value": "1" + }, + { + "dateTime": "22:42:00", + "value": "1" + }, + { + "dateTime": "22:43:00", + "value": "1" + }, + { + "dateTime": "22:44:00", + "value": "1" + }, + { + "dateTime": "22:45:00", + "value": "1" + }, + { + "dateTime": "22:46:00", + "value": "1" + }, + { + "dateTime": "22:47:00", + "value": "1" + }, + { + "dateTime": "22:48:00", + "value": "1" + }, + { + "dateTime": "22:49:00", + "value": "1" + }, + { + "dateTime": "22:50:00", + "value": "1" + }, + { + "dateTime": "22:51:00", + "value": "1" + }, + { + "dateTime": "22:52:00", + "value": "1" + }, + { + "dateTime": "22:53:00", + "value": "1" + }, + { + "dateTime": "22:54:00", + "value": "1" + }, + { + "dateTime": "22:55:00", + "value": "1" + }, + { + "dateTime": "22:56:00", + "value": "1" + }, + { + "dateTime": "22:57:00", + "value": "1" + }, + { + "dateTime": "22:58:00", + "value": "1" + }, + { + "dateTime": "22:59:00", + "value": "1" + }, + { + "dateTime": "23:00:00", + "value": "1" + }, + { + "dateTime": "23:01:00", + "value": "1" + }, + { + "dateTime": "23:02:00", + "value": "1" + }, + { + "dateTime": "23:03:00", + "value": "1" + }, + { + "dateTime": "23:04:00", + "value": "1" + }, + { + "dateTime": "23:05:00", + "value": "1" + }, + { + "dateTime": "23:06:00", + "value": "1" + }, + { + "dateTime": "23:07:00", + "value": "1" + }, + { + "dateTime": "23:08:00", + "value": "1" + }, + { + "dateTime": "23:09:00", + "value": "1" + }, + { + "dateTime": "23:10:00", + "value": "1" + }, + { + "dateTime": "23:11:00", + "value": "1" + }, + { + "dateTime": "23:12:00", + "value": "1" + }, + { + "dateTime": "23:13:00", + "value": "1" + }, + { + "dateTime": "23:14:00", + "value": "1" + }, + { + "dateTime": "23:15:00", + "value": "1" + }, + { + "dateTime": "23:16:00", + "value": "1" + }, + { + "dateTime": "23:17:00", + "value": "1" + }, + { + "dateTime": "23:18:00", + "value": "1" + }, + { + "dateTime": "23:19:00", + "value": "1" + }, + { + "dateTime": "23:20:00", + "value": "1" + }, + { + "dateTime": "23:21:00", + "value": "1" + }, + { + "dateTime": "23:22:00", + "value": "1" + }, + { + "dateTime": "23:23:00", + "value": "1" + }, + { + "dateTime": "23:24:00", + "value": "1" + }, + { + "dateTime": "23:25:00", + "value": "1" + }, + { + "dateTime": "23:26:00", + "value": "1" + }, + { + "dateTime": "23:27:00", + "value": "1" + }, + { + "dateTime": "23:28:00", + "value": "1" + }, + { + "dateTime": "23:29:00", + "value": "1" + }, + { + "dateTime": "23:30:00", + "value": "1" + }, + { + "dateTime": "23:31:00", + "value": "1" + }, + { + "dateTime": "23:32:00", + "value": "1" + }, + { + "dateTime": "23:33:00", + "value": "1" + }, + { + "dateTime": "23:34:00", + "value": "1" + }, + { + "dateTime": "23:35:00", + "value": "1" + }, + { + "dateTime": "23:36:00", + "value": "1" + }, + { + "dateTime": "23:37:00", + "value": "1" + }, + { + "dateTime": "23:38:00", + "value": "1" + }, + { + "dateTime": "23:39:00", + "value": "1" + }, + { + "dateTime": "23:40:00", + "value": "1" + }, + { + "dateTime": "23:41:00", + "value": "1" + }, + { + "dateTime": "23:42:00", + "value": "1" + }, + { + "dateTime": "23:43:00", + "value": "1" + }, + { + "dateTime": "23:44:00", + "value": "1" + }, + { + "dateTime": "23:45:00", + "value": "1" + }, + { + "dateTime": "23:46:00", + "value": "1" + }, + { + "dateTime": "23:47:00", + "value": "1" + }, + { + "dateTime": "23:48:00", + "value": "1" + }, + { + "dateTime": "23:49:00", + "value": "1" + }, + { + "dateTime": "23:50:00", + "value": "1" + }, + { + "dateTime": "23:51:00", + "value": "1" + }, + { + "dateTime": "23:52:00", + "value": "1" + }, + { + "dateTime": "23:53:00", + "value": "1" + }, + { + "dateTime": "23:54:00", + "value": "1" + }, + { + "dateTime": "23:55:00", + "value": "1" + }, + { + "dateTime": "23:56:00", + "value": "1" + }, + { + "dateTime": "23:57:00", + "value": "1" + }, + { + "dateTime": "23:58:00", + "value": "1" + }, + { + "dateTime": "23:59:00", + "value": "1" + }, + { + "dateTime": "00:00:00", + "value": "1" + }, + { + "dateTime": "00:01:00", + "value": "1" + }, + { + "dateTime": "00:02:00", + "value": "1" + }, + { + "dateTime": "00:03:00", + "value": "1" + }, + { + "dateTime": "00:04:00", + "value": "1" + }, + { + "dateTime": "00:05:00", + "value": "1" + }, + { + "dateTime": "00:06:00", + "value": "1" + }, + { + "dateTime": "00:07:00", + "value": "1" + }, + { + "dateTime": "00:08:00", + "value": "1" + }, + { + "dateTime": "00:09:00", + "value": "1" + }, + { + "dateTime": "00:10:00", + "value": "1" + }, + { + "dateTime": "00:11:00", + "value": "1" + }, + { + "dateTime": "00:12:00", + "value": "1" + }, + { + "dateTime": "00:13:00", + "value": "1" + }, + { + "dateTime": "00:14:00", + "value": "1" + }, + { + "dateTime": "00:15:00", + "value": "1" + }, + { + "dateTime": "00:16:00", + "value": "1" + }, + { + "dateTime": "00:17:00", + "value": "1" + }, + { + "dateTime": "00:18:00", + "value": "1" + }, + { + "dateTime": "00:19:00", + "value": "1" + }, + { + "dateTime": "00:20:00", + "value": "1" + }, + { + "dateTime": "00:21:00", + "value": "1" + }, + { + "dateTime": "00:22:00", + "value": "1" + }, + { + "dateTime": "00:23:00", + "value": "1" + }, + { + "dateTime": "00:24:00", + "value": "1" + }, + { + "dateTime": "00:25:00", + "value": "1" + }, + { + "dateTime": "00:26:00", + "value": "1" + }, + { + "dateTime": "00:27:00", + "value": "1" + }, + { + "dateTime": "00:28:00", + "value": "1" + }, + { + "dateTime": "00:29:00", + "value": "1" + }, + { + "dateTime": "00:30:00", + "value": "1" + }, + { + "dateTime": "00:31:00", + "value": "1" + }, + { + "dateTime": "00:32:00", + "value": "1" + }, + { + "dateTime": "00:33:00", + "value": "1" + }, + { + "dateTime": "00:34:00", + "value": "1" + }, + { + "dateTime": "00:35:00", + "value": "1" + }, + { + "dateTime": "00:36:00", + "value": "1" + }, + { + "dateTime": "00:37:00", + "value": "1" + }, + { + "dateTime": "00:38:00", + "value": "1" + }, + { + "dateTime": "00:39:00", + "value": "1" + }, + { + "dateTime": "00:40:00", + "value": "1" + }, + { + "dateTime": "00:41:00", + "value": "1" + }, + { + "dateTime": "00:42:00", + "value": "1" + }, + { + "dateTime": "00:43:00", + "value": "1" + }, + { + "dateTime": "00:44:00", + "value": "1" + }, + { + "dateTime": "00:45:00", + "value": "1" + }, + { + "dateTime": "00:46:00", + "value": "1" + }, + { + "dateTime": "00:47:00", + "value": "1" + }, + { + "dateTime": "00:48:00", + "value": "1" + }, + { + "dateTime": "00:49:00", + "value": "1" + }, + { + "dateTime": "00:50:00", + "value": "1" + }, + { + "dateTime": "00:51:00", + "value": "1" + }, + { + "dateTime": "00:52:00", + "value": "1" + }, + { + "dateTime": "00:53:00", + "value": "1" + }, + { + "dateTime": "00:54:00", + "value": "1" + }, + { + "dateTime": "00:55:00", + "value": "1" + }, + { + "dateTime": "00:56:00", + "value": "1" + }, + { + "dateTime": "00:57:00", + "value": "1" + }, + { + "dateTime": "00:58:00", + "value": "1" + }, + { + "dateTime": "00:59:00", + "value": "1" + }, + { + "dateTime": "01:00:00", + "value": "1" + }, + { + "dateTime": "01:01:00", + "value": "1" + }, + { + "dateTime": "01:02:00", + "value": "1" + }, + { + "dateTime": "01:03:00", + "value": "1" + }, + { + "dateTime": "01:04:00", + "value": "1" + }, + { + "dateTime": "01:05:00", + "value": "1" + }, + { + "dateTime": "01:06:00", + "value": "1" + }, + { + "dateTime": "01:07:00", + "value": "1" + }, + { + "dateTime": "01:08:00", + "value": "1" + }, + { + "dateTime": "01:09:00", + "value": "1" + }, + { + "dateTime": "01:10:00", + "value": "1" + }, + { + "dateTime": "01:11:00", + "value": "1" + }, + { + "dateTime": "01:12:00", + "value": "1" + }, + { + "dateTime": "01:13:00", + "value": "1" + }, + { + "dateTime": "01:14:00", + "value": "1" + }, + { + "dateTime": "01:15:00", + "value": "1" + }, + { + "dateTime": "01:16:00", + "value": "1" + }, + { + "dateTime": "01:17:00", + "value": "1" + }, + { + "dateTime": "01:18:00", + "value": "1" + }, + { + "dateTime": "01:19:00", + "value": "1" + }, + { + "dateTime": "01:20:00", + "value": "1" + }, + { + "dateTime": "01:21:00", + "value": "1" + }, + { + "dateTime": "01:22:00", + "value": "1" + }, + { + "dateTime": "01:23:00", + "value": "1" + }, + { + "dateTime": "01:24:00", + "value": "1" + }, + { + "dateTime": "01:25:00", + "value": "1" + }, + { + "dateTime": "01:26:00", + "value": "1" + }, + { + "dateTime": "01:27:00", + "value": "1" + }, + { + "dateTime": "01:28:00", + "value": "1" + }, + { + "dateTime": "01:29:00", + "value": "1" + }, + { + "dateTime": "01:30:00", + "value": "1" + }, + { + "dateTime": "01:31:00", + "value": "1" + }, + { + "dateTime": "01:32:00", + "value": "1" + }, + { + "dateTime": "01:33:00", + "value": "1" + }, + { + "dateTime": "01:34:00", + "value": "1" + }, + { + "dateTime": "01:35:00", + "value": "1" + }, + { + "dateTime": "01:36:00", + "value": "1" + }, + { + "dateTime": "01:37:00", + "value": "1" + }, + { + "dateTime": "01:38:00", + "value": "1" + }, + { + "dateTime": "01:39:00", + "value": "1" + }, + { + "dateTime": "01:40:00", + "value": "1" + }, + { + "dateTime": "01:41:00", + "value": "1" + }, + { + "dateTime": "01:42:00", + "value": "1" + }, + { + "dateTime": "01:43:00", + "value": "1" + }, + { + "dateTime": "01:44:00", + "value": "1" + }, + { + "dateTime": "01:45:00", + "value": "1" + }, + { + "dateTime": "01:46:00", + "value": "1" + }, + { + "dateTime": "01:47:00", + "value": "1" + }, + { + "dateTime": "01:48:00", + "value": "1" + }, + { + "dateTime": "01:49:00", + "value": "1" + }, + { + "dateTime": "01:50:00", + "value": "1" + }, + { + "dateTime": "01:51:00", + "value": "1" + }, + { + "dateTime": "01:52:00", + "value": "1" + }, + { + "dateTime": "01:53:00", + "value": "1" + }, + { + "dateTime": "01:54:00", + "value": "1" + }, + { + "dateTime": "01:55:00", + "value": "1" + }, + { + "dateTime": "01:56:00", + "value": "1" + }, + { + "dateTime": "01:57:00", + "value": "1" + }, + { + "dateTime": "01:58:00", + "value": "1" + }, + { + "dateTime": "01:59:00", + "value": "1" + }, + { + "dateTime": "02:00:00", + "value": "1" + }, + { + "dateTime": "02:01:00", + "value": "1" + }, + { + "dateTime": "02:02:00", + "value": "1" + }, + { + "dateTime": "02:03:00", + "value": "1" + }, + { + "dateTime": "02:04:00", + "value": "1" + }, + { + "dateTime": "02:05:00", + "value": "1" + }, + { + "dateTime": "02:06:00", + "value": "1" + }, + { + "dateTime": "02:07:00", + "value": "1" + }, + { + "dateTime": "02:08:00", + "value": "1" + }, + { + "dateTime": "02:09:00", + "value": "1" + }, + { + "dateTime": "02:10:00", + "value": "1" + }, + { + "dateTime": "02:11:00", + "value": "1" + }, + { + "dateTime": "02:12:00", + "value": "1" + }, + { + "dateTime": "02:13:00", + "value": "1" + }, + { + "dateTime": "02:14:00", + "value": "1" + }, + { + "dateTime": "02:15:00", + "value": "1" + }, + { + "dateTime": "02:16:00", + "value": "1" + }, + { + "dateTime": "02:17:00", + "value": "1" + }, + { + "dateTime": "02:18:00", + "value": "1" + }, + { + "dateTime": "02:19:00", + "value": "1" + }, + { + "dateTime": "02:20:00", + "value": "1" + }, + { + "dateTime": "02:21:00", + "value": "1" + }, + { + "dateTime": "02:22:00", + "value": "1" + }, + { + "dateTime": "02:23:00", + "value": "1" + }, + { + "dateTime": "02:24:00", + "value": "1" + }, + { + "dateTime": "02:25:00", + "value": "1" + }, + { + "dateTime": "02:26:00", + "value": "1" + }, + { + "dateTime": "02:27:00", + "value": "1" + }, + { + "dateTime": "02:28:00", + "value": "1" + }, + { + "dateTime": "02:29:00", + "value": "1" + }, + { + "dateTime": "02:30:00", + "value": "1" + }, + { + "dateTime": "02:31:00", + "value": "1" + }, + { + "dateTime": "02:32:00", + "value": "1" + }, + { + "dateTime": "02:33:00", + "value": "1" + }, + { + "dateTime": "02:34:00", + "value": "1" + }, + { + "dateTime": "02:35:00", + "value": "1" + }, + { + "dateTime": "02:36:00", + "value": "1" + }, + { + "dateTime": "02:37:00", + "value": "1" + }, + { + "dateTime": "02:38:00", + "value": "1" + }, + { + "dateTime": "02:39:00", + "value": "1" + }, + { + "dateTime": "02:40:00", + "value": "1" + }, + { + "dateTime": "02:41:00", + "value": "1" + }, + { + "dateTime": "02:42:00", + "value": "1" + }, + { + "dateTime": "02:43:00", + "value": "1" + }, + { + "dateTime": "02:44:00", + "value": "1" + }, + { + "dateTime": "02:45:00", + "value": "1" + }, + { + "dateTime": "02:46:00", + "value": "1" + }, + { + "dateTime": "02:47:00", + "value": "1" + }, + { + "dateTime": "02:48:00", + "value": "1" + }, + { + "dateTime": "02:49:00", + "value": "1" + }, + { + "dateTime": "02:50:00", + "value": "1" + }, + { + "dateTime": "02:51:00", + "value": "1" + }, + { + "dateTime": "02:52:00", + "value": "1" + }, + { + "dateTime": "02:53:00", + "value": "1" + }, + { + "dateTime": "02:54:00", + "value": "1" + }, + { + "dateTime": "02:55:00", + "value": "1" + }, + { + "dateTime": "02:56:00", + "value": "1" + }, + { + "dateTime": "02:57:00", + "value": "1" + }, + { + "dateTime": "02:58:00", + "value": "1" + }, + { + "dateTime": "02:59:00", + "value": "1" + }, + { + "dateTime": "03:00:00", + "value": "1" + }, + { + "dateTime": "03:01:00", + "value": "1" + }, + { + "dateTime": "03:02:00", + "value": "1" + }, + { + "dateTime": "03:03:00", + "value": "1" + }, + { + "dateTime": "03:04:00", + "value": "1" + }, + { + "dateTime": "03:05:00", + "value": "1" + }, + { + "dateTime": "03:06:00", + "value": "1" + }, + { + "dateTime": "03:07:00", + "value": "1" + }, + { + "dateTime": "03:08:00", + "value": "1" + }, + { + "dateTime": "03:09:00", + "value": "1" + }, + { + "dateTime": "03:10:00", + "value": "1" + }, + { + "dateTime": "03:11:00", + "value": "1" + }, + { + "dateTime": "03:12:00", + "value": "1" + }, + { + "dateTime": "03:13:00", + "value": "1" + }, + { + "dateTime": "03:14:00", + "value": "1" + }, + { + "dateTime": "03:15:00", + "value": "1" + }, + { + "dateTime": "03:16:00", + "value": "1" + }, + { + "dateTime": "03:17:00", + "value": "1" + }, + { + "dateTime": "03:18:00", + "value": "1" + }, + { + "dateTime": "03:19:00", + "value": "1" + }, + { + "dateTime": "03:20:00", + "value": "1" + }, + { + "dateTime": "03:21:00", + "value": "1" + }, + { + "dateTime": "03:22:00", + "value": "1" + }, + { + "dateTime": "03:23:00", + "value": "1" + }, + { + "dateTime": "03:24:00", + "value": "1" + }, + { + "dateTime": "03:25:00", + "value": "1" + }, + { + "dateTime": "03:26:00", + "value": "1" + }, + { + "dateTime": "03:27:00", + "value": "1" + }, + { + "dateTime": "03:28:00", + "value": "1" + }, + { + "dateTime": "03:29:00", + "value": "1" + }, + { + "dateTime": "03:30:00", + "value": "1" + }, + { + "dateTime": "03:31:00", + "value": "1" + }, + { + "dateTime": "03:32:00", + "value": "1" + }, + { + "dateTime": "03:33:00", + "value": "1" + }, + { + "dateTime": "03:34:00", + "value": "1" + }, + { + "dateTime": "03:35:00", + "value": "1" + }, + { + "dateTime": "03:36:00", + "value": "1" + }, + { + "dateTime": "03:37:00", + "value": "1" + }, + { + "dateTime": "03:38:00", + "value": "1" + }, + { + "dateTime": "03:39:00", + "value": "1" + }, + { + "dateTime": "03:40:00", + "value": "1" + }, + { + "dateTime": "03:41:00", + "value": "1" + }, + { + "dateTime": "03:42:00", + "value": "1" + }, + { + "dateTime": "03:43:00", + "value": "1" + }, + { + "dateTime": "03:44:00", + "value": "1" + }, + { + "dateTime": "03:45:00", + "value": "1" + }, + { + "dateTime": "03:46:00", + "value": "1" + }, + { + "dateTime": "03:47:00", + "value": "1" + }, + { + "dateTime": "03:48:00", + "value": "1" + }, + { + "dateTime": "03:49:00", + "value": "1" + }, + { + "dateTime": "03:50:00", + "value": "1" + }, + { + "dateTime": "03:51:00", + "value": "1" + }, + { + "dateTime": "03:52:00", + "value": "1" + }, + { + "dateTime": "03:53:00", + "value": "1" + }, + { + "dateTime": "03:54:00", + "value": "1" + }, + { + "dateTime": "03:55:00", + "value": "1" + }, + { + "dateTime": "03:56:00", + "value": "1" + }, + { + "dateTime": "03:57:00", + "value": "1" + }, + { + "dateTime": "03:58:00", + "value": "1" + }, + { + "dateTime": "03:59:00", + "value": "1" + }, + { + "dateTime": "04:00:00", + "value": "1" + }, + { + "dateTime": "04:01:00", + "value": "1" + }, + { + "dateTime": "04:02:00", + "value": "1" + }, + { + "dateTime": "04:03:00", + "value": "1" + }, + { + "dateTime": "04:04:00", + "value": "1" + }, + { + "dateTime": "04:05:00", + "value": "1" + }, + { + "dateTime": "04:06:00", + "value": "1" + }, + { + "dateTime": "04:07:00", + "value": "1" + }, + { + "dateTime": "04:08:00", + "value": "1" + }, + { + "dateTime": "04:09:00", + "value": "1" + }, + { + "dateTime": "04:10:00", + "value": "1" + }, + { + "dateTime": "04:11:00", + "value": "1" + }, + { + "dateTime": "04:12:00", + "value": "1" + }, + { + "dateTime": "04:13:00", + "value": "1" + }, + { + "dateTime": "04:14:00", + "value": "1" + }, + { + "dateTime": "04:15:00", + "value": "1" + }, + { + "dateTime": "04:16:00", + "value": "1" + }, + { + "dateTime": "04:17:00", + "value": "1" + }, + { + "dateTime": "04:18:00", + "value": "1" + }, + { + "dateTime": "04:19:00", + "value": "1" + }, + { + "dateTime": "04:20:00", + "value": "1" + }, + { + "dateTime": "04:21:00", + "value": "1" + }, + { + "dateTime": "04:22:00", + "value": "1" + }, + { + "dateTime": "04:23:00", + "value": "1" + }, + { + "dateTime": "04:24:00", + "value": "1" + }, + { + "dateTime": "04:25:00", + "value": "1" + }, + { + "dateTime": "04:26:00", + "value": "1" + }, + { + "dateTime": "04:27:00", + "value": "1" + }, + { + "dateTime": "04:28:00", + "value": "1" + }, + { + "dateTime": "04:29:00", + "value": "1" + }, + { + "dateTime": "04:30:00", + "value": "1" + }, + { + "dateTime": "04:31:00", + "value": "1" + }, + { + "dateTime": "04:32:00", + "value": "1" + }, + { + "dateTime": "04:33:00", + "value": "1" + }, + { + "dateTime": "04:34:00", + "value": "1" + }, + { + "dateTime": "04:35:00", + "value": "1" + }, + { + "dateTime": "04:36:00", + "value": "1" + }, + { + "dateTime": "04:37:00", + "value": "1" + }, + { + "dateTime": "04:38:00", + "value": "1" + }, + { + "dateTime": "04:39:00", + "value": "1" + }, + { + "dateTime": "04:40:00", + "value": "1" + }, + { + "dateTime": "04:41:00", + "value": "1" + }, + { + "dateTime": "04:42:00", + "value": "1" + }, + { + "dateTime": "04:43:00", + "value": "1" + }, + { + "dateTime": "04:44:00", + "value": "1" + }, + { + "dateTime": "04:45:00", + "value": "1" + }, + { + "dateTime": "04:46:00", + "value": "1" + }, + { + "dateTime": "04:47:00", + "value": "1" + }, + { + "dateTime": "04:48:00", + "value": "1" + }, + { + "dateTime": "04:49:00", + "value": "1" + }, + { + "dateTime": "04:50:00", + "value": "1" + }, + { + "dateTime": "04:51:00", + "value": "1" + }, + { + "dateTime": "04:52:00", + "value": "1" + }, + { + "dateTime": "04:53:00", + "value": "1" + }, + { + "dateTime": "04:54:00", + "value": "1" + }, + { + "dateTime": "04:55:00", + "value": "1" + }, + { + "dateTime": "04:56:00", + "value": "1" + }, + { + "dateTime": "04:57:00", + "value": "1" + }, + { + "dateTime": "04:58:00", + "value": "1" + }, + { + "dateTime": "04:59:00", + "value": "1" + }, + { + "dateTime": "05:00:00", + "value": "1" + }, + { + "dateTime": "05:01:00", + "value": "1" + }, + { + "dateTime": "05:02:00", + "value": "1" + }, + { + "dateTime": "05:03:00", + "value": "1" + }, + { + "dateTime": "05:04:00", + "value": "1" + }, + { + "dateTime": "05:05:00", + "value": "1" + }, + { + "dateTime": "05:06:00", + "value": "1" + }, + { + "dateTime": "05:07:00", + "value": "1" + }, + { + "dateTime": "05:08:00", + "value": "1" + }, + { + "dateTime": "05:09:00", + "value": "1" + }, + { + "dateTime": "05:10:00", + "value": "1" + }, + { + "dateTime": "05:11:00", + "value": "1" + }, + { + "dateTime": "05:12:00", + "value": "1" + }, + { + "dateTime": "05:13:00", + "value": "1" + }, + { + "dateTime": "05:14:00", + "value": "1" + }, + { + "dateTime": "05:15:00", + "value": "1" + }, + { + "dateTime": "05:16:00", + "value": "1" + }, + { + "dateTime": "05:17:00", + "value": "1" + }, + { + "dateTime": "05:18:00", + "value": "1" + }, + { + "dateTime": "05:19:00", + "value": "1" + }, + { + "dateTime": "05:20:00", + "value": "1" + }, + { + "dateTime": "05:21:00", + "value": "1" + }, + { + "dateTime": "05:22:00", + "value": "1" + }, + { + "dateTime": "05:23:00", + "value": "1" + }, + { + "dateTime": "05:24:00", + "value": "1" + }, + { + "dateTime": "05:25:00", + "value": "1" + }, + { + "dateTime": "05:26:00", + "value": "1" + }, + { + "dateTime": "05:27:00", + "value": "1" + }, + { + "dateTime": "05:28:00", + "value": "1" + }, + { + "dateTime": "05:29:00", + "value": "1" + }, + { + "dateTime": "05:30:00", + "value": "1" + }, + { + "dateTime": "05:31:00", + "value": "1" + }, + { + "dateTime": "05:32:00", + "value": "1" + }, + { + "dateTime": "05:33:00", + "value": "1" + }, + { + "dateTime": "05:34:00", + "value": "1" + }, + { + "dateTime": "05:35:00", + "value": "1" + }, + { + "dateTime": "05:36:00", + "value": "1" + }, + { + "dateTime": "05:37:00", + "value": "1" + }, + { + "dateTime": "05:38:00", + "value": "1" + }, + { + "dateTime": "05:39:00", + "value": "1" + }, + { + "dateTime": "05:40:00", + "value": "1" + }, + { + "dateTime": "05:41:00", + "value": "1" + }, + { + "dateTime": "05:42:00", + "value": "1" + }, + { + "dateTime": "05:43:00", + "value": "1" + }, + { + "dateTime": "05:44:00", + "value": "1" + }, + { + "dateTime": "05:45:00", + "value": "1" + }, + { + "dateTime": "05:46:00", + "value": "1" + }, + { + "dateTime": "05:47:00", + "value": "1" + }, + { + "dateTime": "05:48:00", + "value": "1" + }, + { + "dateTime": "05:49:00", + "value": "1" + }, + { + "dateTime": "05:50:00", + "value": "1" + }, + { + "dateTime": "05:51:00", + "value": "1" + }, + { + "dateTime": "05:52:00", + "value": "1" + }, + { + "dateTime": "05:53:00", + "value": "1" + }, + { + "dateTime": "05:54:00", + "value": "1" + }, + { + "dateTime": "05:55:00", + "value": "1" + }, + { + "dateTime": "05:56:00", + "value": "1" + }, + { + "dateTime": "05:57:00", + "value": "1" + }, + { + "dateTime": "05:58:00", + "value": "1" + }, + { + "dateTime": "05:59:00", + "value": "1" + } + ], + "minutesAfterWakeup": 0, + "minutesAsleep": 480, + "minutesAwake": 0, + "minutesToFallAsleep": 0, + "restlessCount": 0, + "restlessDuration": 0, + "startTime": "2015-06-07T22:00:00.000", + "timeInBed": 480 + } + ], + "summary": { + "totalMinutesAsleep": 570, + "totalSleepRecords": 2, + "totalTimeInBed": 570 + } +} \ No newline at end of file diff --git a/shim-server/src/test/resources/org/openmhealth/shim/fitbit/mapper/fitbit-get-sleep.json b/shim-server/src/test/resources/org/openmhealth/shim/fitbit/mapper/fitbit-get-sleep.json new file mode 100644 index 00000000..2153fa41 --- /dev/null +++ b/shim-server/src/test/resources/org/openmhealth/shim/fitbit/mapper/fitbit-get-sleep.json @@ -0,0 +1,3873 @@ +{ + "sleep": [ + { + "awakeCount": 25, + "awakeDuration": 70, + "awakeningsCount": 25, + "dateOfSleep": "2014-07-20", + "duration": 57660000, + "efficiency": 87, + "isMainSleep": true, + "logId": 216325800, + "minuteData": [ + { + "dateTime": "11:58:00", + "value": "3" + }, + { + "dateTime": "11:59:00", + "value": "3" + }, + { + "dateTime": "12:00:00", + "value": "3" + }, + { + "dateTime": "12:01:00", + "value": "3" + }, + { + "dateTime": "12:02:00", + "value": "3" + }, + { + "dateTime": "12:03:00", + "value": "3" + }, + { + "dateTime": "12:04:00", + "value": "2" + }, + { + "dateTime": "12:05:00", + "value": "2" + }, + { + "dateTime": "12:06:00", + "value": "2" + }, + { + "dateTime": "12:07:00", + "value": "2" + }, + { + "dateTime": "12:08:00", + "value": "2" + }, + { + "dateTime": "12:09:00", + "value": "1" + }, + { + "dateTime": "12:10:00", + "value": "1" + }, + { + "dateTime": "12:11:00", + "value": "1" + }, + { + "dateTime": "12:12:00", + "value": "1" + }, + { + "dateTime": "12:13:00", + "value": "1" + }, + { + "dateTime": "12:14:00", + "value": "1" + }, + { + "dateTime": "12:15:00", + "value": "1" + }, + { + "dateTime": "12:16:00", + "value": "1" + }, + { + "dateTime": "12:17:00", + "value": "1" + }, + { + "dateTime": "12:18:00", + "value": "1" + }, + { + "dateTime": "12:19:00", + "value": "1" + }, + { + "dateTime": "12:20:00", + "value": "1" + }, + { + "dateTime": "12:21:00", + "value": "1" + }, + { + "dateTime": "12:22:00", + "value": "1" + }, + { + "dateTime": "12:23:00", + "value": "1" + }, + { + "dateTime": "12:24:00", + "value": "1" + }, + { + "dateTime": "12:25:00", + "value": "2" + }, + { + "dateTime": "12:26:00", + "value": "1" + }, + { + "dateTime": "12:27:00", + "value": "1" + }, + { + "dateTime": "12:28:00", + "value": "1" + }, + { + "dateTime": "12:29:00", + "value": "3" + }, + { + "dateTime": "12:30:00", + "value": "1" + }, + { + "dateTime": "12:31:00", + "value": "1" + }, + { + "dateTime": "12:32:00", + "value": "1" + }, + { + "dateTime": "12:33:00", + "value": "1" + }, + { + "dateTime": "12:34:00", + "value": "1" + }, + { + "dateTime": "12:35:00", + "value": "1" + }, + { + "dateTime": "12:36:00", + "value": "1" + }, + { + "dateTime": "12:37:00", + "value": "1" + }, + { + "dateTime": "12:38:00", + "value": "1" + }, + { + "dateTime": "12:39:00", + "value": "1" + }, + { + "dateTime": "12:40:00", + "value": "1" + }, + { + "dateTime": "12:41:00", + "value": "1" + }, + { + "dateTime": "12:42:00", + "value": "1" + }, + { + "dateTime": "12:43:00", + "value": "1" + }, + { + "dateTime": "12:44:00", + "value": "1" + }, + { + "dateTime": "12:45:00", + "value": "1" + }, + { + "dateTime": "12:46:00", + "value": "1" + }, + { + "dateTime": "12:47:00", + "value": "1" + }, + { + "dateTime": "12:48:00", + "value": "1" + }, + { + "dateTime": "12:49:00", + "value": "1" + }, + { + "dateTime": "12:50:00", + "value": "1" + }, + { + "dateTime": "12:51:00", + "value": "1" + }, + { + "dateTime": "12:52:00", + "value": "1" + }, + { + "dateTime": "12:53:00", + "value": "1" + }, + { + "dateTime": "12:54:00", + "value": "1" + }, + { + "dateTime": "12:55:00", + "value": "1" + }, + { + "dateTime": "12:56:00", + "value": "1" + }, + { + "dateTime": "12:57:00", + "value": "1" + }, + { + "dateTime": "12:58:00", + "value": "1" + }, + { + "dateTime": "12:59:00", + "value": "1" + }, + { + "dateTime": "13:00:00", + "value": "1" + }, + { + "dateTime": "13:01:00", + "value": "1" + }, + { + "dateTime": "13:02:00", + "value": "1" + }, + { + "dateTime": "13:03:00", + "value": "1" + }, + { + "dateTime": "13:04:00", + "value": "1" + }, + { + "dateTime": "13:05:00", + "value": "1" + }, + { + "dateTime": "13:06:00", + "value": "1" + }, + { + "dateTime": "13:07:00", + "value": "1" + }, + { + "dateTime": "13:08:00", + "value": "1" + }, + { + "dateTime": "13:09:00", + "value": "1" + }, + { + "dateTime": "13:10:00", + "value": "1" + }, + { + "dateTime": "13:11:00", + "value": "3" + }, + { + "dateTime": "13:12:00", + "value": "1" + }, + { + "dateTime": "13:13:00", + "value": "1" + }, + { + "dateTime": "13:14:00", + "value": "1" + }, + { + "dateTime": "13:15:00", + "value": "1" + }, + { + "dateTime": "13:16:00", + "value": "1" + }, + { + "dateTime": "13:17:00", + "value": "1" + }, + { + "dateTime": "13:18:00", + "value": "1" + }, + { + "dateTime": "13:19:00", + "value": "1" + }, + { + "dateTime": "13:20:00", + "value": "1" + }, + { + "dateTime": "13:21:00", + "value": "1" + }, + { + "dateTime": "13:22:00", + "value": "1" + }, + { + "dateTime": "13:23:00", + "value": "1" + }, + { + "dateTime": "13:24:00", + "value": "1" + }, + { + "dateTime": "13:25:00", + "value": "1" + }, + { + "dateTime": "13:26:00", + "value": "3" + }, + { + "dateTime": "13:27:00", + "value": "3" + }, + { + "dateTime": "13:28:00", + "value": "1" + }, + { + "dateTime": "13:29:00", + "value": "3" + }, + { + "dateTime": "13:30:00", + "value": "3" + }, + { + "dateTime": "13:31:00", + "value": "1" + }, + { + "dateTime": "13:32:00", + "value": "1" + }, + { + "dateTime": "13:33:00", + "value": "1" + }, + { + "dateTime": "13:34:00", + "value": "1" + }, + { + "dateTime": "13:35:00", + "value": "1" + }, + { + "dateTime": "13:36:00", + "value": "1" + }, + { + "dateTime": "13:37:00", + "value": "2" + }, + { + "dateTime": "13:38:00", + "value": "2" + }, + { + "dateTime": "13:39:00", + "value": "1" + }, + { + "dateTime": "13:40:00", + "value": "1" + }, + { + "dateTime": "13:41:00", + "value": "1" + }, + { + "dateTime": "13:42:00", + "value": "1" + }, + { + "dateTime": "13:43:00", + "value": "2" + }, + { + "dateTime": "13:44:00", + "value": "2" + }, + { + "dateTime": "13:45:00", + "value": "2" + }, + { + "dateTime": "13:46:00", + "value": "2" + }, + { + "dateTime": "13:47:00", + "value": "1" + }, + { + "dateTime": "13:48:00", + "value": "1" + }, + { + "dateTime": "13:49:00", + "value": "1" + }, + { + "dateTime": "13:50:00", + "value": "2" + }, + { + "dateTime": "13:51:00", + "value": "2" + }, + { + "dateTime": "13:52:00", + "value": "1" + }, + { + "dateTime": "13:53:00", + "value": "1" + }, + { + "dateTime": "13:54:00", + "value": "3" + }, + { + "dateTime": "13:55:00", + "value": "3" + }, + { + "dateTime": "13:56:00", + "value": "1" + }, + { + "dateTime": "13:57:00", + "value": "1" + }, + { + "dateTime": "13:58:00", + "value": "1" + }, + { + "dateTime": "13:59:00", + "value": "3" + }, + { + "dateTime": "14:00:00", + "value": "3" + }, + { + "dateTime": "14:01:00", + "value": "3" + }, + { + "dateTime": "14:02:00", + "value": "3" + }, + { + "dateTime": "14:03:00", + "value": "2" + }, + { + "dateTime": "14:04:00", + "value": "3" + }, + { + "dateTime": "14:05:00", + "value": "1" + }, + { + "dateTime": "14:06:00", + "value": "1" + }, + { + "dateTime": "14:07:00", + "value": "1" + }, + { + "dateTime": "14:08:00", + "value": "1" + }, + { + "dateTime": "14:09:00", + "value": "1" + }, + { + "dateTime": "14:10:00", + "value": "1" + }, + { + "dateTime": "14:11:00", + "value": "1" + }, + { + "dateTime": "14:12:00", + "value": "1" + }, + { + "dateTime": "14:13:00", + "value": "1" + }, + { + "dateTime": "14:14:00", + "value": "1" + }, + { + "dateTime": "14:15:00", + "value": "1" + }, + { + "dateTime": "14:16:00", + "value": "1" + }, + { + "dateTime": "14:17:00", + "value": "1" + }, + { + "dateTime": "14:18:00", + "value": "1" + }, + { + "dateTime": "14:19:00", + "value": "1" + }, + { + "dateTime": "14:20:00", + "value": "1" + }, + { + "dateTime": "14:21:00", + "value": "1" + }, + { + "dateTime": "14:22:00", + "value": "1" + }, + { + "dateTime": "14:23:00", + "value": "1" + }, + { + "dateTime": "14:24:00", + "value": "1" + }, + { + "dateTime": "14:25:00", + "value": "1" + }, + { + "dateTime": "14:26:00", + "value": "1" + }, + { + "dateTime": "14:27:00", + "value": "1" + }, + { + "dateTime": "14:28:00", + "value": "1" + }, + { + "dateTime": "14:29:00", + "value": "1" + }, + { + "dateTime": "14:30:00", + "value": "1" + }, + { + "dateTime": "14:31:00", + "value": "1" + }, + { + "dateTime": "14:32:00", + "value": "1" + }, + { + "dateTime": "14:33:00", + "value": "1" + }, + { + "dateTime": "14:34:00", + "value": "1" + }, + { + "dateTime": "14:35:00", + "value": "1" + }, + { + "dateTime": "14:36:00", + "value": "1" + }, + { + "dateTime": "14:37:00", + "value": "1" + }, + { + "dateTime": "14:38:00", + "value": "1" + }, + { + "dateTime": "14:39:00", + "value": "1" + }, + { + "dateTime": "14:40:00", + "value": "1" + }, + { + "dateTime": "14:41:00", + "value": "1" + }, + { + "dateTime": "14:42:00", + "value": "1" + }, + { + "dateTime": "14:43:00", + "value": "3" + }, + { + "dateTime": "14:44:00", + "value": "3" + }, + { + "dateTime": "14:45:00", + "value": "3" + }, + { + "dateTime": "14:46:00", + "value": "3" + }, + { + "dateTime": "14:47:00", + "value": "3" + }, + { + "dateTime": "14:48:00", + "value": "1" + }, + { + "dateTime": "14:49:00", + "value": "1" + }, + { + "dateTime": "14:50:00", + "value": "1" + }, + { + "dateTime": "14:51:00", + "value": "1" + }, + { + "dateTime": "14:52:00", + "value": "1" + }, + { + "dateTime": "14:53:00", + "value": "1" + }, + { + "dateTime": "14:54:00", + "value": "2" + }, + { + "dateTime": "14:55:00", + "value": "1" + }, + { + "dateTime": "14:56:00", + "value": "1" + }, + { + "dateTime": "14:57:00", + "value": "2" + }, + { + "dateTime": "14:58:00", + "value": "2" + }, + { + "dateTime": "14:59:00", + "value": "2" + }, + { + "dateTime": "15:00:00", + "value": "3" + }, + { + "dateTime": "15:01:00", + "value": "3" + }, + { + "dateTime": "15:02:00", + "value": "3" + }, + { + "dateTime": "15:03:00", + "value": "3" + }, + { + "dateTime": "15:04:00", + "value": "3" + }, + { + "dateTime": "15:05:00", + "value": "3" + }, + { + "dateTime": "15:06:00", + "value": "1" + }, + { + "dateTime": "15:07:00", + "value": "1" + }, + { + "dateTime": "15:08:00", + "value": "1" + }, + { + "dateTime": "15:09:00", + "value": "1" + }, + { + "dateTime": "15:10:00", + "value": "1" + }, + { + "dateTime": "15:11:00", + "value": "1" + }, + { + "dateTime": "15:12:00", + "value": "1" + }, + { + "dateTime": "15:13:00", + "value": "1" + }, + { + "dateTime": "15:14:00", + "value": "1" + }, + { + "dateTime": "15:15:00", + "value": "1" + }, + { + "dateTime": "15:16:00", + "value": "3" + }, + { + "dateTime": "15:17:00", + "value": "3" + }, + { + "dateTime": "15:18:00", + "value": "1" + }, + { + "dateTime": "15:19:00", + "value": "2" + }, + { + "dateTime": "15:20:00", + "value": "2" + }, + { + "dateTime": "15:21:00", + "value": "2" + }, + { + "dateTime": "15:22:00", + "value": "3" + }, + { + "dateTime": "15:23:00", + "value": "3" + }, + { + "dateTime": "15:24:00", + "value": "3" + }, + { + "dateTime": "15:25:00", + "value": "3" + }, + { + "dateTime": "15:26:00", + "value": "2" + }, + { + "dateTime": "15:27:00", + "value": "1" + }, + { + "dateTime": "15:28:00", + "value": "3" + }, + { + "dateTime": "15:29:00", + "value": "3" + }, + { + "dateTime": "15:30:00", + "value": "3" + }, + { + "dateTime": "15:31:00", + "value": "3" + }, + { + "dateTime": "15:32:00", + "value": "3" + }, + { + "dateTime": "15:33:00", + "value": "3" + }, + { + "dateTime": "15:34:00", + "value": "1" + }, + { + "dateTime": "15:35:00", + "value": "1" + }, + { + "dateTime": "15:36:00", + "value": "1" + }, + { + "dateTime": "15:37:00", + "value": "1" + }, + { + "dateTime": "15:38:00", + "value": "1" + }, + { + "dateTime": "15:39:00", + "value": "1" + }, + { + "dateTime": "15:40:00", + "value": "1" + }, + { + "dateTime": "15:41:00", + "value": "2" + }, + { + "dateTime": "15:42:00", + "value": "2" + }, + { + "dateTime": "15:43:00", + "value": "3" + }, + { + "dateTime": "15:44:00", + "value": "3" + }, + { + "dateTime": "15:45:00", + "value": "2" + }, + { + "dateTime": "15:46:00", + "value": "1" + }, + { + "dateTime": "15:47:00", + "value": "1" + }, + { + "dateTime": "15:48:00", + "value": "2" + }, + { + "dateTime": "15:49:00", + "value": "3" + }, + { + "dateTime": "15:50:00", + "value": "2" + }, + { + "dateTime": "15:51:00", + "value": "3" + }, + { + "dateTime": "15:52:00", + "value": "2" + }, + { + "dateTime": "15:53:00", + "value": "3" + }, + { + "dateTime": "15:54:00", + "value": "3" + }, + { + "dateTime": "15:55:00", + "value": "3" + }, + { + "dateTime": "15:56:00", + "value": "3" + }, + { + "dateTime": "15:57:00", + "value": "1" + }, + { + "dateTime": "15:58:00", + "value": "1" + }, + { + "dateTime": "15:59:00", + "value": "1" + }, + { + "dateTime": "16:00:00", + "value": "1" + }, + { + "dateTime": "16:01:00", + "value": "1" + }, + { + "dateTime": "16:02:00", + "value": "1" + }, + { + "dateTime": "16:03:00", + "value": "1" + }, + { + "dateTime": "16:04:00", + "value": "1" + }, + { + "dateTime": "16:05:00", + "value": "1" + }, + { + "dateTime": "16:06:00", + "value": "1" + }, + { + "dateTime": "16:07:00", + "value": "1" + }, + { + "dateTime": "16:08:00", + "value": "1" + }, + { + "dateTime": "16:09:00", + "value": "1" + }, + { + "dateTime": "16:10:00", + "value": "1" + }, + { + "dateTime": "16:11:00", + "value": "1" + }, + { + "dateTime": "16:12:00", + "value": "1" + }, + { + "dateTime": "16:13:00", + "value": "1" + }, + { + "dateTime": "16:14:00", + "value": "1" + }, + { + "dateTime": "16:15:00", + "value": "1" + }, + { + "dateTime": "16:16:00", + "value": "1" + }, + { + "dateTime": "16:17:00", + "value": "1" + }, + { + "dateTime": "16:18:00", + "value": "2" + }, + { + "dateTime": "16:19:00", + "value": "1" + }, + { + "dateTime": "16:20:00", + "value": "1" + }, + { + "dateTime": "16:21:00", + "value": "1" + }, + { + "dateTime": "16:22:00", + "value": "1" + }, + { + "dateTime": "16:23:00", + "value": "1" + }, + { + "dateTime": "16:24:00", + "value": "1" + }, + { + "dateTime": "16:25:00", + "value": "1" + }, + { + "dateTime": "16:26:00", + "value": "3" + }, + { + "dateTime": "16:27:00", + "value": "3" + }, + { + "dateTime": "16:28:00", + "value": "1" + }, + { + "dateTime": "16:29:00", + "value": "1" + }, + { + "dateTime": "16:30:00", + "value": "1" + }, + { + "dateTime": "16:31:00", + "value": "1" + }, + { + "dateTime": "16:32:00", + "value": "3" + }, + { + "dateTime": "16:33:00", + "value": "3" + }, + { + "dateTime": "16:34:00", + "value": "2" + }, + { + "dateTime": "16:35:00", + "value": "2" + }, + { + "dateTime": "16:36:00", + "value": "3" + }, + { + "dateTime": "16:37:00", + "value": "3" + }, + { + "dateTime": "16:38:00", + "value": "3" + }, + { + "dateTime": "16:39:00", + "value": "2" + }, + { + "dateTime": "16:40:00", + "value": "3" + }, + { + "dateTime": "16:41:00", + "value": "1" + }, + { + "dateTime": "16:42:00", + "value": "1" + }, + { + "dateTime": "16:43:00", + "value": "1" + }, + { + "dateTime": "16:44:00", + "value": "2" + }, + { + "dateTime": "16:45:00", + "value": "2" + }, + { + "dateTime": "16:46:00", + "value": "1" + }, + { + "dateTime": "16:47:00", + "value": "1" + }, + { + "dateTime": "16:48:00", + "value": "3" + }, + { + "dateTime": "16:49:00", + "value": "3" + }, + { + "dateTime": "16:50:00", + "value": "2" + }, + { + "dateTime": "16:51:00", + "value": "2" + }, + { + "dateTime": "16:52:00", + "value": "2" + }, + { + "dateTime": "16:53:00", + "value": "2" + }, + { + "dateTime": "16:54:00", + "value": "3" + }, + { + "dateTime": "16:55:00", + "value": "3" + }, + { + "dateTime": "16:56:00", + "value": "3" + }, + { + "dateTime": "16:57:00", + "value": "3" + }, + { + "dateTime": "16:58:00", + "value": "1" + }, + { + "dateTime": "16:59:00", + "value": "1" + }, + { + "dateTime": "17:00:00", + "value": "2" + }, + { + "dateTime": "17:01:00", + "value": "2" + }, + { + "dateTime": "17:02:00", + "value": "3" + }, + { + "dateTime": "17:03:00", + "value": "3" + }, + { + "dateTime": "17:04:00", + "value": "1" + }, + { + "dateTime": "17:05:00", + "value": "1" + }, + { + "dateTime": "17:06:00", + "value": "1" + }, + { + "dateTime": "17:07:00", + "value": "1" + }, + { + "dateTime": "17:08:00", + "value": "1" + }, + { + "dateTime": "17:09:00", + "value": "1" + }, + { + "dateTime": "17:10:00", + "value": "1" + }, + { + "dateTime": "17:11:00", + "value": "1" + }, + { + "dateTime": "17:12:00", + "value": "1" + }, + { + "dateTime": "17:13:00", + "value": "1" + }, + { + "dateTime": "17:14:00", + "value": "1" + }, + { + "dateTime": "17:15:00", + "value": "1" + }, + { + "dateTime": "17:16:00", + "value": "1" + }, + { + "dateTime": "17:17:00", + "value": "1" + }, + { + "dateTime": "17:18:00", + "value": "1" + }, + { + "dateTime": "17:19:00", + "value": "1" + }, + { + "dateTime": "17:20:00", + "value": "1" + }, + { + "dateTime": "17:21:00", + "value": "1" + }, + { + "dateTime": "17:22:00", + "value": "1" + }, + { + "dateTime": "17:23:00", + "value": "1" + }, + { + "dateTime": "17:24:00", + "value": "1" + }, + { + "dateTime": "17:25:00", + "value": "1" + }, + { + "dateTime": "17:26:00", + "value": "1" + }, + { + "dateTime": "17:27:00", + "value": "1" + }, + { + "dateTime": "17:28:00", + "value": "1" + }, + { + "dateTime": "17:29:00", + "value": "1" + }, + { + "dateTime": "17:30:00", + "value": "1" + }, + { + "dateTime": "17:31:00", + "value": "1" + }, + { + "dateTime": "17:32:00", + "value": "1" + }, + { + "dateTime": "17:33:00", + "value": "1" + }, + { + "dateTime": "17:34:00", + "value": "1" + }, + { + "dateTime": "17:35:00", + "value": "1" + }, + { + "dateTime": "17:36:00", + "value": "1" + }, + { + "dateTime": "17:37:00", + "value": "1" + }, + { + "dateTime": "17:38:00", + "value": "1" + }, + { + "dateTime": "17:39:00", + "value": "1" + }, + { + "dateTime": "17:40:00", + "value": "1" + }, + { + "dateTime": "17:41:00", + "value": "1" + }, + { + "dateTime": "17:42:00", + "value": "1" + }, + { + "dateTime": "17:43:00", + "value": "1" + }, + { + "dateTime": "17:44:00", + "value": "1" + }, + { + "dateTime": "17:45:00", + "value": "1" + }, + { + "dateTime": "17:46:00", + "value": "1" + }, + { + "dateTime": "17:47:00", + "value": "1" + }, + { + "dateTime": "17:48:00", + "value": "1" + }, + { + "dateTime": "17:49:00", + "value": "1" + }, + { + "dateTime": "17:50:00", + "value": "1" + }, + { + "dateTime": "17:51:00", + "value": "1" + }, + { + "dateTime": "17:52:00", + "value": "1" + }, + { + "dateTime": "17:53:00", + "value": "1" + }, + { + "dateTime": "17:54:00", + "value": "1" + }, + { + "dateTime": "17:55:00", + "value": "1" + }, + { + "dateTime": "17:56:00", + "value": "1" + }, + { + "dateTime": "17:57:00", + "value": "1" + }, + { + "dateTime": "17:58:00", + "value": "1" + }, + { + "dateTime": "17:59:00", + "value": "1" + }, + { + "dateTime": "18:00:00", + "value": "1" + }, + { + "dateTime": "18:01:00", + "value": "1" + }, + { + "dateTime": "18:02:00", + "value": "1" + }, + { + "dateTime": "18:03:00", + "value": "1" + }, + { + "dateTime": "18:04:00", + "value": "1" + }, + { + "dateTime": "18:05:00", + "value": "1" + }, + { + "dateTime": "18:06:00", + "value": "1" + }, + { + "dateTime": "18:07:00", + "value": "1" + }, + { + "dateTime": "18:08:00", + "value": "1" + }, + { + "dateTime": "18:09:00", + "value": "1" + }, + { + "dateTime": "18:10:00", + "value": "1" + }, + { + "dateTime": "18:11:00", + "value": "1" + }, + { + "dateTime": "18:12:00", + "value": "1" + }, + { + "dateTime": "18:13:00", + "value": "1" + }, + { + "dateTime": "18:14:00", + "value": "1" + }, + { + "dateTime": "18:15:00", + "value": "1" + }, + { + "dateTime": "18:16:00", + "value": "1" + }, + { + "dateTime": "18:17:00", + "value": "1" + }, + { + "dateTime": "18:18:00", + "value": "1" + }, + { + "dateTime": "18:19:00", + "value": "1" + }, + { + "dateTime": "18:20:00", + "value": "1" + }, + { + "dateTime": "18:21:00", + "value": "1" + }, + { + "dateTime": "18:22:00", + "value": "1" + }, + { + "dateTime": "18:23:00", + "value": "1" + }, + { + "dateTime": "18:24:00", + "value": "1" + }, + { + "dateTime": "18:25:00", + "value": "1" + }, + { + "dateTime": "18:26:00", + "value": "1" + }, + { + "dateTime": "18:27:00", + "value": "1" + }, + { + "dateTime": "18:28:00", + "value": "1" + }, + { + "dateTime": "18:29:00", + "value": "1" + }, + { + "dateTime": "18:30:00", + "value": "1" + }, + { + "dateTime": "18:31:00", + "value": "1" + }, + { + "dateTime": "18:32:00", + "value": "1" + }, + { + "dateTime": "18:33:00", + "value": "1" + }, + { + "dateTime": "18:34:00", + "value": "1" + }, + { + "dateTime": "18:35:00", + "value": "1" + }, + { + "dateTime": "18:36:00", + "value": "1" + }, + { + "dateTime": "18:37:00", + "value": "1" + }, + { + "dateTime": "18:38:00", + "value": "1" + }, + { + "dateTime": "18:39:00", + "value": "1" + }, + { + "dateTime": "18:40:00", + "value": "1" + }, + { + "dateTime": "18:41:00", + "value": "1" + }, + { + "dateTime": "18:42:00", + "value": "1" + }, + { + "dateTime": "18:43:00", + "value": "1" + }, + { + "dateTime": "18:44:00", + "value": "1" + }, + { + "dateTime": "18:45:00", + "value": "1" + }, + { + "dateTime": "18:46:00", + "value": "1" + }, + { + "dateTime": "18:47:00", + "value": "1" + }, + { + "dateTime": "18:48:00", + "value": "1" + }, + { + "dateTime": "18:49:00", + "value": "1" + }, + { + "dateTime": "18:50:00", + "value": "1" + }, + { + "dateTime": "18:51:00", + "value": "1" + }, + { + "dateTime": "18:52:00", + "value": "1" + }, + { + "dateTime": "18:53:00", + "value": "1" + }, + { + "dateTime": "18:54:00", + "value": "1" + }, + { + "dateTime": "18:55:00", + "value": "1" + }, + { + "dateTime": "18:56:00", + "value": "1" + }, + { + "dateTime": "18:57:00", + "value": "1" + }, + { + "dateTime": "18:58:00", + "value": "1" + }, + { + "dateTime": "18:59:00", + "value": "1" + }, + { + "dateTime": "19:00:00", + "value": "1" + }, + { + "dateTime": "19:01:00", + "value": "1" + }, + { + "dateTime": "19:02:00", + "value": "1" + }, + { + "dateTime": "19:03:00", + "value": "1" + }, + { + "dateTime": "19:04:00", + "value": "1" + }, + { + "dateTime": "19:05:00", + "value": "1" + }, + { + "dateTime": "19:06:00", + "value": "1" + }, + { + "dateTime": "19:07:00", + "value": "1" + }, + { + "dateTime": "19:08:00", + "value": "1" + }, + { + "dateTime": "19:09:00", + "value": "1" + }, + { + "dateTime": "19:10:00", + "value": "1" + }, + { + "dateTime": "19:11:00", + "value": "1" + }, + { + "dateTime": "19:12:00", + "value": "1" + }, + { + "dateTime": "19:13:00", + "value": "1" + }, + { + "dateTime": "19:14:00", + "value": "1" + }, + { + "dateTime": "19:15:00", + "value": "1" + }, + { + "dateTime": "19:16:00", + "value": "1" + }, + { + "dateTime": "19:17:00", + "value": "1" + }, + { + "dateTime": "19:18:00", + "value": "1" + }, + { + "dateTime": "19:19:00", + "value": "1" + }, + { + "dateTime": "19:20:00", + "value": "1" + }, + { + "dateTime": "19:21:00", + "value": "1" + }, + { + "dateTime": "19:22:00", + "value": "1" + }, + { + "dateTime": "19:23:00", + "value": "1" + }, + { + "dateTime": "19:24:00", + "value": "1" + }, + { + "dateTime": "19:25:00", + "value": "1" + }, + { + "dateTime": "19:26:00", + "value": "1" + }, + { + "dateTime": "19:27:00", + "value": "1" + }, + { + "dateTime": "19:28:00", + "value": "1" + }, + { + "dateTime": "19:29:00", + "value": "1" + }, + { + "dateTime": "19:30:00", + "value": "1" + }, + { + "dateTime": "19:31:00", + "value": "1" + }, + { + "dateTime": "19:32:00", + "value": "1" + }, + { + "dateTime": "19:33:00", + "value": "1" + }, + { + "dateTime": "19:34:00", + "value": "1" + }, + { + "dateTime": "19:35:00", + "value": "1" + }, + { + "dateTime": "19:36:00", + "value": "1" + }, + { + "dateTime": "19:37:00", + "value": "1" + }, + { + "dateTime": "19:38:00", + "value": "1" + }, + { + "dateTime": "19:39:00", + "value": "1" + }, + { + "dateTime": "19:40:00", + "value": "1" + }, + { + "dateTime": "19:41:00", + "value": "1" + }, + { + "dateTime": "19:42:00", + "value": "1" + }, + { + "dateTime": "19:43:00", + "value": "1" + }, + { + "dateTime": "19:44:00", + "value": "1" + }, + { + "dateTime": "19:45:00", + "value": "1" + }, + { + "dateTime": "19:46:00", + "value": "1" + }, + { + "dateTime": "19:47:00", + "value": "1" + }, + { + "dateTime": "19:48:00", + "value": "1" + }, + { + "dateTime": "19:49:00", + "value": "1" + }, + { + "dateTime": "19:50:00", + "value": "1" + }, + { + "dateTime": "19:51:00", + "value": "1" + }, + { + "dateTime": "19:52:00", + "value": "1" + }, + { + "dateTime": "19:53:00", + "value": "1" + }, + { + "dateTime": "19:54:00", + "value": "1" + }, + { + "dateTime": "19:55:00", + "value": "1" + }, + { + "dateTime": "19:56:00", + "value": "1" + }, + { + "dateTime": "19:57:00", + "value": "1" + }, + { + "dateTime": "19:58:00", + "value": "1" + }, + { + "dateTime": "19:59:00", + "value": "1" + }, + { + "dateTime": "20:00:00", + "value": "1" + }, + { + "dateTime": "20:01:00", + "value": "1" + }, + { + "dateTime": "20:02:00", + "value": "1" + }, + { + "dateTime": "20:03:00", + "value": "1" + }, + { + "dateTime": "20:04:00", + "value": "1" + }, + { + "dateTime": "20:05:00", + "value": "1" + }, + { + "dateTime": "20:06:00", + "value": "1" + }, + { + "dateTime": "20:07:00", + "value": "1" + }, + { + "dateTime": "20:08:00", + "value": "1" + }, + { + "dateTime": "20:09:00", + "value": "1" + }, + { + "dateTime": "20:10:00", + "value": "1" + }, + { + "dateTime": "20:11:00", + "value": "1" + }, + { + "dateTime": "20:12:00", + "value": "1" + }, + { + "dateTime": "20:13:00", + "value": "1" + }, + { + "dateTime": "20:14:00", + "value": "1" + }, + { + "dateTime": "20:15:00", + "value": "1" + }, + { + "dateTime": "20:16:00", + "value": "1" + }, + { + "dateTime": "20:17:00", + "value": "1" + }, + { + "dateTime": "20:18:00", + "value": "1" + }, + { + "dateTime": "20:19:00", + "value": "1" + }, + { + "dateTime": "20:20:00", + "value": "1" + }, + { + "dateTime": "20:21:00", + "value": "1" + }, + { + "dateTime": "20:22:00", + "value": "1" + }, + { + "dateTime": "20:23:00", + "value": "1" + }, + { + "dateTime": "20:24:00", + "value": "1" + }, + { + "dateTime": "20:25:00", + "value": "1" + }, + { + "dateTime": "20:26:00", + "value": "1" + }, + { + "dateTime": "20:27:00", + "value": "1" + }, + { + "dateTime": "20:28:00", + "value": "1" + }, + { + "dateTime": "20:29:00", + "value": "1" + }, + { + "dateTime": "20:30:00", + "value": "1" + }, + { + "dateTime": "20:31:00", + "value": "1" + }, + { + "dateTime": "20:32:00", + "value": "1" + }, + { + "dateTime": "20:33:00", + "value": "1" + }, + { + "dateTime": "20:34:00", + "value": "1" + }, + { + "dateTime": "20:35:00", + "value": "1" + }, + { + "dateTime": "20:36:00", + "value": "1" + }, + { + "dateTime": "20:37:00", + "value": "1" + }, + { + "dateTime": "20:38:00", + "value": "1" + }, + { + "dateTime": "20:39:00", + "value": "1" + }, + { + "dateTime": "20:40:00", + "value": "1" + }, + { + "dateTime": "20:41:00", + "value": "1" + }, + { + "dateTime": "20:42:00", + "value": "1" + }, + { + "dateTime": "20:43:00", + "value": "1" + }, + { + "dateTime": "20:44:00", + "value": "1" + }, + { + "dateTime": "20:45:00", + "value": "1" + }, + { + "dateTime": "20:46:00", + "value": "1" + }, + { + "dateTime": "20:47:00", + "value": "1" + }, + { + "dateTime": "20:48:00", + "value": "1" + }, + { + "dateTime": "20:49:00", + "value": "1" + }, + { + "dateTime": "20:50:00", + "value": "1" + }, + { + "dateTime": "20:51:00", + "value": "1" + }, + { + "dateTime": "20:52:00", + "value": "1" + }, + { + "dateTime": "20:53:00", + "value": "1" + }, + { + "dateTime": "20:54:00", + "value": "1" + }, + { + "dateTime": "20:55:00", + "value": "1" + }, + { + "dateTime": "20:56:00", + "value": "1" + }, + { + "dateTime": "20:57:00", + "value": "1" + }, + { + "dateTime": "20:58:00", + "value": "1" + }, + { + "dateTime": "20:59:00", + "value": "1" + }, + { + "dateTime": "21:00:00", + "value": "1" + }, + { + "dateTime": "21:01:00", + "value": "1" + }, + { + "dateTime": "21:02:00", + "value": "1" + }, + { + "dateTime": "21:03:00", + "value": "1" + }, + { + "dateTime": "21:04:00", + "value": "1" + }, + { + "dateTime": "21:05:00", + "value": "1" + }, + { + "dateTime": "21:06:00", + "value": "1" + }, + { + "dateTime": "21:07:00", + "value": "1" + }, + { + "dateTime": "21:08:00", + "value": "1" + }, + { + "dateTime": "21:09:00", + "value": "1" + }, + { + "dateTime": "21:10:00", + "value": "1" + }, + { + "dateTime": "21:11:00", + "value": "1" + }, + { + "dateTime": "21:12:00", + "value": "1" + }, + { + "dateTime": "21:13:00", + "value": "1" + }, + { + "dateTime": "21:14:00", + "value": "1" + }, + { + "dateTime": "21:15:00", + "value": "1" + }, + { + "dateTime": "21:16:00", + "value": "1" + }, + { + "dateTime": "21:17:00", + "value": "1" + }, + { + "dateTime": "21:18:00", + "value": "1" + }, + { + "dateTime": "21:19:00", + "value": "1" + }, + { + "dateTime": "21:20:00", + "value": "1" + }, + { + "dateTime": "21:21:00", + "value": "1" + }, + { + "dateTime": "21:22:00", + "value": "1" + }, + { + "dateTime": "21:23:00", + "value": "1" + }, + { + "dateTime": "21:24:00", + "value": "1" + }, + { + "dateTime": "21:25:00", + "value": "1" + }, + { + "dateTime": "21:26:00", + "value": "1" + }, + { + "dateTime": "21:27:00", + "value": "1" + }, + { + "dateTime": "21:28:00", + "value": "1" + }, + { + "dateTime": "21:29:00", + "value": "1" + }, + { + "dateTime": "21:30:00", + "value": "1" + }, + { + "dateTime": "21:31:00", + "value": "1" + }, + { + "dateTime": "21:32:00", + "value": "1" + }, + { + "dateTime": "21:33:00", + "value": "1" + }, + { + "dateTime": "21:34:00", + "value": "1" + }, + { + "dateTime": "21:35:00", + "value": "1" + }, + { + "dateTime": "21:36:00", + "value": "1" + }, + { + "dateTime": "21:37:00", + "value": "1" + }, + { + "dateTime": "21:38:00", + "value": "1" + }, + { + "dateTime": "21:39:00", + "value": "1" + }, + { + "dateTime": "21:40:00", + "value": "1" + }, + { + "dateTime": "21:41:00", + "value": "1" + }, + { + "dateTime": "21:42:00", + "value": "1" + }, + { + "dateTime": "21:43:00", + "value": "1" + }, + { + "dateTime": "21:44:00", + "value": "1" + }, + { + "dateTime": "21:45:00", + "value": "1" + }, + { + "dateTime": "21:46:00", + "value": "1" + }, + { + "dateTime": "21:47:00", + "value": "1" + }, + { + "dateTime": "21:48:00", + "value": "1" + }, + { + "dateTime": "21:49:00", + "value": "1" + }, + { + "dateTime": "21:50:00", + "value": "1" + }, + { + "dateTime": "21:51:00", + "value": "1" + }, + { + "dateTime": "21:52:00", + "value": "1" + }, + { + "dateTime": "21:53:00", + "value": "1" + }, + { + "dateTime": "21:54:00", + "value": "1" + }, + { + "dateTime": "21:55:00", + "value": "1" + }, + { + "dateTime": "21:56:00", + "value": "1" + }, + { + "dateTime": "21:57:00", + "value": "1" + }, + { + "dateTime": "21:58:00", + "value": "1" + }, + { + "dateTime": "21:59:00", + "value": "1" + }, + { + "dateTime": "22:00:00", + "value": "1" + }, + { + "dateTime": "22:01:00", + "value": "1" + }, + { + "dateTime": "22:02:00", + "value": "1" + }, + { + "dateTime": "22:03:00", + "value": "1" + }, + { + "dateTime": "22:04:00", + "value": "1" + }, + { + "dateTime": "22:05:00", + "value": "1" + }, + { + "dateTime": "22:06:00", + "value": "1" + }, + { + "dateTime": "22:07:00", + "value": "1" + }, + { + "dateTime": "22:08:00", + "value": "1" + }, + { + "dateTime": "22:09:00", + "value": "1" + }, + { + "dateTime": "22:10:00", + "value": "1" + }, + { + "dateTime": "22:11:00", + "value": "1" + }, + { + "dateTime": "22:12:00", + "value": "1" + }, + { + "dateTime": "22:13:00", + "value": "1" + }, + { + "dateTime": "22:14:00", + "value": "1" + }, + { + "dateTime": "22:15:00", + "value": "1" + }, + { + "dateTime": "22:16:00", + "value": "1" + }, + { + "dateTime": "22:17:00", + "value": "1" + }, + { + "dateTime": "22:18:00", + "value": "1" + }, + { + "dateTime": "22:19:00", + "value": "1" + }, + { + "dateTime": "22:20:00", + "value": "1" + }, + { + "dateTime": "22:21:00", + "value": "1" + }, + { + "dateTime": "22:22:00", + "value": "1" + }, + { + "dateTime": "22:23:00", + "value": "1" + }, + { + "dateTime": "22:24:00", + "value": "1" + }, + { + "dateTime": "22:25:00", + "value": "1" + }, + { + "dateTime": "22:26:00", + "value": "1" + }, + { + "dateTime": "22:27:00", + "value": "1" + }, + { + "dateTime": "22:28:00", + "value": "1" + }, + { + "dateTime": "22:29:00", + "value": "1" + }, + { + "dateTime": "22:30:00", + "value": "1" + }, + { + "dateTime": "22:31:00", + "value": "1" + }, + { + "dateTime": "22:32:00", + "value": "1" + }, + { + "dateTime": "22:33:00", + "value": "1" + }, + { + "dateTime": "22:34:00", + "value": "1" + }, + { + "dateTime": "22:35:00", + "value": "1" + }, + { + "dateTime": "22:36:00", + "value": "1" + }, + { + "dateTime": "22:37:00", + "value": "1" + }, + { + "dateTime": "22:38:00", + "value": "1" + }, + { + "dateTime": "22:39:00", + "value": "1" + }, + { + "dateTime": "22:40:00", + "value": "1" + }, + { + "dateTime": "22:41:00", + "value": "1" + }, + { + "dateTime": "22:42:00", + "value": "1" + }, + { + "dateTime": "22:43:00", + "value": "1" + }, + { + "dateTime": "22:44:00", + "value": "1" + }, + { + "dateTime": "22:45:00", + "value": "1" + }, + { + "dateTime": "22:46:00", + "value": "1" + }, + { + "dateTime": "22:47:00", + "value": "1" + }, + { + "dateTime": "22:48:00", + "value": "1" + }, + { + "dateTime": "22:49:00", + "value": "1" + }, + { + "dateTime": "22:50:00", + "value": "1" + }, + { + "dateTime": "22:51:00", + "value": "1" + }, + { + "dateTime": "22:52:00", + "value": "1" + }, + { + "dateTime": "22:53:00", + "value": "1" + }, + { + "dateTime": "22:54:00", + "value": "1" + }, + { + "dateTime": "22:55:00", + "value": "1" + }, + { + "dateTime": "22:56:00", + "value": "1" + }, + { + "dateTime": "22:57:00", + "value": "1" + }, + { + "dateTime": "22:58:00", + "value": "1" + }, + { + "dateTime": "22:59:00", + "value": "1" + }, + { + "dateTime": "23:00:00", + "value": "1" + }, + { + "dateTime": "23:01:00", + "value": "1" + }, + { + "dateTime": "23:02:00", + "value": "1" + }, + { + "dateTime": "23:03:00", + "value": "1" + }, + { + "dateTime": "23:04:00", + "value": "1" + }, + { + "dateTime": "23:05:00", + "value": "1" + }, + { + "dateTime": "23:06:00", + "value": "1" + }, + { + "dateTime": "23:07:00", + "value": "1" + }, + { + "dateTime": "23:08:00", + "value": "1" + }, + { + "dateTime": "23:09:00", + "value": "1" + }, + { + "dateTime": "23:10:00", + "value": "1" + }, + { + "dateTime": "23:11:00", + "value": "1" + }, + { + "dateTime": "23:12:00", + "value": "1" + }, + { + "dateTime": "23:13:00", + "value": "1" + }, + { + "dateTime": "23:14:00", + "value": "1" + }, + { + "dateTime": "23:15:00", + "value": "1" + }, + { + "dateTime": "23:16:00", + "value": "1" + }, + { + "dateTime": "23:17:00", + "value": "1" + }, + { + "dateTime": "23:18:00", + "value": "1" + }, + { + "dateTime": "23:19:00", + "value": "1" + }, + { + "dateTime": "23:20:00", + "value": "1" + }, + { + "dateTime": "23:21:00", + "value": "1" + }, + { + "dateTime": "23:22:00", + "value": "1" + }, + { + "dateTime": "23:23:00", + "value": "1" + }, + { + "dateTime": "23:24:00", + "value": "1" + }, + { + "dateTime": "23:25:00", + "value": "1" + }, + { + "dateTime": "23:26:00", + "value": "1" + }, + { + "dateTime": "23:27:00", + "value": "1" + }, + { + "dateTime": "23:28:00", + "value": "1" + }, + { + "dateTime": "23:29:00", + "value": "1" + }, + { + "dateTime": "23:30:00", + "value": "1" + }, + { + "dateTime": "23:31:00", + "value": "1" + }, + { + "dateTime": "23:32:00", + "value": "1" + }, + { + "dateTime": "23:33:00", + "value": "1" + }, + { + "dateTime": "23:34:00", + "value": "1" + }, + { + "dateTime": "23:35:00", + "value": "1" + }, + { + "dateTime": "23:36:00", + "value": "1" + }, + { + "dateTime": "23:37:00", + "value": "1" + }, + { + "dateTime": "23:38:00", + "value": "1" + }, + { + "dateTime": "23:39:00", + "value": "1" + }, + { + "dateTime": "23:40:00", + "value": "1" + }, + { + "dateTime": "23:41:00", + "value": "1" + }, + { + "dateTime": "23:42:00", + "value": "1" + }, + { + "dateTime": "23:43:00", + "value": "1" + }, + { + "dateTime": "23:44:00", + "value": "1" + }, + { + "dateTime": "23:45:00", + "value": "1" + }, + { + "dateTime": "23:46:00", + "value": "1" + }, + { + "dateTime": "23:47:00", + "value": "1" + }, + { + "dateTime": "23:48:00", + "value": "1" + }, + { + "dateTime": "23:49:00", + "value": "1" + }, + { + "dateTime": "23:50:00", + "value": "1" + }, + { + "dateTime": "23:51:00", + "value": "1" + }, + { + "dateTime": "23:52:00", + "value": "1" + }, + { + "dateTime": "23:53:00", + "value": "1" + }, + { + "dateTime": "23:54:00", + "value": "1" + }, + { + "dateTime": "23:55:00", + "value": "1" + }, + { + "dateTime": "23:56:00", + "value": "1" + }, + { + "dateTime": "23:57:00", + "value": "1" + }, + { + "dateTime": "23:58:00", + "value": "1" + }, + { + "dateTime": "23:59:00", + "value": "1" + }, + { + "dateTime": "00:00:00", + "value": "1" + }, + { + "dateTime": "00:01:00", + "value": "1" + }, + { + "dateTime": "00:02:00", + "value": "1" + }, + { + "dateTime": "00:03:00", + "value": "1" + }, + { + "dateTime": "00:04:00", + "value": "1" + }, + { + "dateTime": "00:05:00", + "value": "1" + }, + { + "dateTime": "00:06:00", + "value": "1" + }, + { + "dateTime": "00:07:00", + "value": "1" + }, + { + "dateTime": "00:08:00", + "value": "1" + }, + { + "dateTime": "00:09:00", + "value": "1" + }, + { + "dateTime": "00:10:00", + "value": "1" + }, + { + "dateTime": "00:11:00", + "value": "1" + }, + { + "dateTime": "00:12:00", + "value": "1" + }, + { + "dateTime": "00:13:00", + "value": "1" + }, + { + "dateTime": "00:14:00", + "value": "1" + }, + { + "dateTime": "00:15:00", + "value": "1" + }, + { + "dateTime": "00:16:00", + "value": "1" + }, + { + "dateTime": "00:17:00", + "value": "1" + }, + { + "dateTime": "00:18:00", + "value": "1" + }, + { + "dateTime": "00:19:00", + "value": "1" + }, + { + "dateTime": "00:20:00", + "value": "1" + }, + { + "dateTime": "00:21:00", + "value": "1" + }, + { + "dateTime": "00:22:00", + "value": "1" + }, + { + "dateTime": "00:23:00", + "value": "1" + }, + { + "dateTime": "00:24:00", + "value": "1" + }, + { + "dateTime": "00:25:00", + "value": "1" + }, + { + "dateTime": "00:26:00", + "value": "1" + }, + { + "dateTime": "00:27:00", + "value": "1" + }, + { + "dateTime": "00:28:00", + "value": "1" + }, + { + "dateTime": "00:29:00", + "value": "1" + }, + { + "dateTime": "00:30:00", + "value": "1" + }, + { + "dateTime": "00:31:00", + "value": "1" + }, + { + "dateTime": "00:32:00", + "value": "1" + }, + { + "dateTime": "00:33:00", + "value": "1" + }, + { + "dateTime": "00:34:00", + "value": "1" + }, + { + "dateTime": "00:35:00", + "value": "1" + }, + { + "dateTime": "00:36:00", + "value": "1" + }, + { + "dateTime": "00:37:00", + "value": "1" + }, + { + "dateTime": "00:38:00", + "value": "1" + }, + { + "dateTime": "00:39:00", + "value": "1" + }, + { + "dateTime": "00:40:00", + "value": "1" + }, + { + "dateTime": "00:41:00", + "value": "1" + }, + { + "dateTime": "00:42:00", + "value": "3" + }, + { + "dateTime": "00:43:00", + "value": "3" + }, + { + "dateTime": "00:44:00", + "value": "1" + }, + { + "dateTime": "00:45:00", + "value": "1" + }, + { + "dateTime": "00:46:00", + "value": "1" + }, + { + "dateTime": "00:47:00", + "value": "1" + }, + { + "dateTime": "00:48:00", + "value": "1" + }, + { + "dateTime": "00:49:00", + "value": "1" + }, + { + "dateTime": "00:50:00", + "value": "1" + }, + { + "dateTime": "00:51:00", + "value": "1" + }, + { + "dateTime": "00:52:00", + "value": "2" + }, + { + "dateTime": "00:53:00", + "value": "2" + }, + { + "dateTime": "00:54:00", + "value": "1" + }, + { + "dateTime": "00:55:00", + "value": "1" + }, + { + "dateTime": "00:56:00", + "value": "1" + }, + { + "dateTime": "00:57:00", + "value": "1" + }, + { + "dateTime": "00:58:00", + "value": "1" + }, + { + "dateTime": "00:59:00", + "value": "3" + }, + { + "dateTime": "01:00:00", + "value": "3" + }, + { + "dateTime": "01:01:00", + "value": "1" + }, + { + "dateTime": "01:02:00", + "value": "1" + }, + { + "dateTime": "01:03:00", + "value": "1" + }, + { + "dateTime": "01:04:00", + "value": "1" + }, + { + "dateTime": "01:05:00", + "value": "1" + }, + { + "dateTime": "01:06:00", + "value": "1" + }, + { + "dateTime": "01:07:00", + "value": "1" + }, + { + "dateTime": "01:08:00", + "value": "1" + }, + { + "dateTime": "01:09:00", + "value": "1" + }, + { + "dateTime": "01:10:00", + "value": "1" + }, + { + "dateTime": "01:11:00", + "value": "1" + }, + { + "dateTime": "01:12:00", + "value": "1" + }, + { + "dateTime": "01:13:00", + "value": "1" + }, + { + "dateTime": "01:14:00", + "value": "1" + }, + { + "dateTime": "01:15:00", + "value": "1" + }, + { + "dateTime": "01:16:00", + "value": "1" + }, + { + "dateTime": "01:17:00", + "value": "1" + }, + { + "dateTime": "01:18:00", + "value": "1" + }, + { + "dateTime": "01:19:00", + "value": "1" + }, + { + "dateTime": "01:20:00", + "value": "1" + }, + { + "dateTime": "01:21:00", + "value": "1" + }, + { + "dateTime": "01:22:00", + "value": "1" + }, + { + "dateTime": "01:23:00", + "value": "1" + }, + { + "dateTime": "01:24:00", + "value": "1" + }, + { + "dateTime": "01:25:00", + "value": "1" + }, + { + "dateTime": "01:26:00", + "value": "1" + }, + { + "dateTime": "01:27:00", + "value": "1" + }, + { + "dateTime": "01:28:00", + "value": "1" + }, + { + "dateTime": "01:29:00", + "value": "1" + }, + { + "dateTime": "01:30:00", + "value": "1" + }, + { + "dateTime": "01:31:00", + "value": "1" + }, + { + "dateTime": "01:32:00", + "value": "1" + }, + { + "dateTime": "01:33:00", + "value": "1" + }, + { + "dateTime": "01:34:00", + "value": "1" + }, + { + "dateTime": "01:35:00", + "value": "1" + }, + { + "dateTime": "01:36:00", + "value": "1" + }, + { + "dateTime": "01:37:00", + "value": "1" + }, + { + "dateTime": "01:38:00", + "value": "1" + }, + { + "dateTime": "01:39:00", + "value": "1" + }, + { + "dateTime": "01:40:00", + "value": "1" + }, + { + "dateTime": "01:41:00", + "value": "1" + }, + { + "dateTime": "01:42:00", + "value": "1" + }, + { + "dateTime": "01:43:00", + "value": "1" + }, + { + "dateTime": "01:44:00", + "value": "1" + }, + { + "dateTime": "01:45:00", + "value": "1" + }, + { + "dateTime": "01:46:00", + "value": "1" + }, + { + "dateTime": "01:47:00", + "value": "1" + }, + { + "dateTime": "01:48:00", + "value": "1" + }, + { + "dateTime": "01:49:00", + "value": "1" + }, + { + "dateTime": "01:50:00", + "value": "1" + }, + { + "dateTime": "01:51:00", + "value": "1" + }, + { + "dateTime": "01:52:00", + "value": "1" + }, + { + "dateTime": "01:53:00", + "value": "1" + }, + { + "dateTime": "01:54:00", + "value": "1" + }, + { + "dateTime": "01:55:00", + "value": "1" + }, + { + "dateTime": "01:56:00", + "value": "1" + }, + { + "dateTime": "01:57:00", + "value": "1" + }, + { + "dateTime": "01:58:00", + "value": "1" + }, + { + "dateTime": "01:59:00", + "value": "1" + }, + { + "dateTime": "02:00:00", + "value": "1" + }, + { + "dateTime": "02:01:00", + "value": "1" + }, + { + "dateTime": "02:02:00", + "value": "1" + }, + { + "dateTime": "02:03:00", + "value": "1" + }, + { + "dateTime": "02:04:00", + "value": "1" + }, + { + "dateTime": "02:05:00", + "value": "1" + }, + { + "dateTime": "02:06:00", + "value": "1" + }, + { + "dateTime": "02:07:00", + "value": "1" + }, + { + "dateTime": "02:08:00", + "value": "1" + }, + { + "dateTime": "02:09:00", + "value": "1" + }, + { + "dateTime": "02:10:00", + "value": "1" + }, + { + "dateTime": "02:11:00", + "value": "1" + }, + { + "dateTime": "02:12:00", + "value": "1" + }, + { + "dateTime": "02:13:00", + "value": "1" + }, + { + "dateTime": "02:14:00", + "value": "1" + }, + { + "dateTime": "02:15:00", + "value": "1" + }, + { + "dateTime": "02:16:00", + "value": "1" + }, + { + "dateTime": "02:17:00", + "value": "1" + }, + { + "dateTime": "02:18:00", + "value": "1" + }, + { + "dateTime": "02:19:00", + "value": "1" + }, + { + "dateTime": "02:20:00", + "value": "1" + }, + { + "dateTime": "02:21:00", + "value": "1" + }, + { + "dateTime": "02:22:00", + "value": "1" + }, + { + "dateTime": "02:23:00", + "value": "1" + }, + { + "dateTime": "02:24:00", + "value": "1" + }, + { + "dateTime": "02:25:00", + "value": "1" + }, + { + "dateTime": "02:26:00", + "value": "1" + }, + { + "dateTime": "02:27:00", + "value": "1" + }, + { + "dateTime": "02:28:00", + "value": "1" + }, + { + "dateTime": "02:29:00", + "value": "1" + }, + { + "dateTime": "02:30:00", + "value": "1" + }, + { + "dateTime": "02:31:00", + "value": "1" + }, + { + "dateTime": "02:32:00", + "value": "1" + }, + { + "dateTime": "02:33:00", + "value": "1" + }, + { + "dateTime": "02:34:00", + "value": "1" + }, + { + "dateTime": "02:35:00", + "value": "1" + }, + { + "dateTime": "02:36:00", + "value": "1" + }, + { + "dateTime": "02:37:00", + "value": "1" + }, + { + "dateTime": "02:38:00", + "value": "1" + }, + { + "dateTime": "02:39:00", + "value": "1" + }, + { + "dateTime": "02:40:00", + "value": "1" + }, + { + "dateTime": "02:41:00", + "value": "1" + }, + { + "dateTime": "02:42:00", + "value": "1" + }, + { + "dateTime": "02:43:00", + "value": "1" + }, + { + "dateTime": "02:44:00", + "value": "1" + }, + { + "dateTime": "02:45:00", + "value": "1" + }, + { + "dateTime": "02:46:00", + "value": "1" + }, + { + "dateTime": "02:47:00", + "value": "1" + }, + { + "dateTime": "02:48:00", + "value": "1" + }, + { + "dateTime": "02:49:00", + "value": "1" + }, + { + "dateTime": "02:50:00", + "value": "1" + }, + { + "dateTime": "02:51:00", + "value": "1" + }, + { + "dateTime": "02:52:00", + "value": "1" + }, + { + "dateTime": "02:53:00", + "value": "1" + }, + { + "dateTime": "02:54:00", + "value": "1" + }, + { + "dateTime": "02:55:00", + "value": "1" + }, + { + "dateTime": "02:56:00", + "value": "1" + }, + { + "dateTime": "02:57:00", + "value": "1" + }, + { + "dateTime": "02:58:00", + "value": "1" + }, + { + "dateTime": "02:59:00", + "value": "1" + }, + { + "dateTime": "03:00:00", + "value": "1" + }, + { + "dateTime": "03:01:00", + "value": "1" + }, + { + "dateTime": "03:02:00", + "value": "1" + }, + { + "dateTime": "03:03:00", + "value": "1" + }, + { + "dateTime": "03:04:00", + "value": "1" + }, + { + "dateTime": "03:05:00", + "value": "1" + }, + { + "dateTime": "03:06:00", + "value": "1" + }, + { + "dateTime": "03:07:00", + "value": "1" + }, + { + "dateTime": "03:08:00", + "value": "1" + }, + { + "dateTime": "03:09:00", + "value": "1" + }, + { + "dateTime": "03:10:00", + "value": "1" + }, + { + "dateTime": "03:11:00", + "value": "1" + }, + { + "dateTime": "03:12:00", + "value": "1" + }, + { + "dateTime": "03:13:00", + "value": "1" + }, + { + "dateTime": "03:14:00", + "value": "1" + }, + { + "dateTime": "03:15:00", + "value": "1" + }, + { + "dateTime": "03:16:00", + "value": "1" + }, + { + "dateTime": "03:17:00", + "value": "1" + }, + { + "dateTime": "03:18:00", + "value": "1" + }, + { + "dateTime": "03:19:00", + "value": "1" + }, + { + "dateTime": "03:20:00", + "value": "1" + }, + { + "dateTime": "03:21:00", + "value": "1" + }, + { + "dateTime": "03:22:00", + "value": "1" + }, + { + "dateTime": "03:23:00", + "value": "1" + }, + { + "dateTime": "03:24:00", + "value": "1" + }, + { + "dateTime": "03:25:00", + "value": "1" + }, + { + "dateTime": "03:26:00", + "value": "1" + }, + { + "dateTime": "03:27:00", + "value": "1" + }, + { + "dateTime": "03:28:00", + "value": "1" + }, + { + "dateTime": "03:29:00", + "value": "1" + }, + { + "dateTime": "03:30:00", + "value": "1" + }, + { + "dateTime": "03:31:00", + "value": "1" + }, + { + "dateTime": "03:32:00", + "value": "1" + }, + { + "dateTime": "03:33:00", + "value": "1" + }, + { + "dateTime": "03:34:00", + "value": "1" + }, + { + "dateTime": "03:35:00", + "value": "1" + }, + { + "dateTime": "03:36:00", + "value": "1" + }, + { + "dateTime": "03:37:00", + "value": "1" + }, + { + "dateTime": "03:38:00", + "value": "1" + }, + { + "dateTime": "03:39:00", + "value": "1" + }, + { + "dateTime": "03:40:00", + "value": "1" + }, + { + "dateTime": "03:41:00", + "value": "1" + }, + { + "dateTime": "03:42:00", + "value": "1" + }, + { + "dateTime": "03:43:00", + "value": "1" + }, + { + "dateTime": "03:44:00", + "value": "1" + }, + { + "dateTime": "03:45:00", + "value": "1" + }, + { + "dateTime": "03:46:00", + "value": "1" + }, + { + "dateTime": "03:47:00", + "value": "1" + }, + { + "dateTime": "03:48:00", + "value": "1" + }, + { + "dateTime": "03:49:00", + "value": "1" + }, + { + "dateTime": "03:50:00", + "value": "1" + }, + { + "dateTime": "03:51:00", + "value": "1" + }, + { + "dateTime": "03:52:00", + "value": "1" + }, + { + "dateTime": "03:53:00", + "value": "1" + }, + { + "dateTime": "03:54:00", + "value": "1" + }, + { + "dateTime": "03:55:00", + "value": "1" + }, + { + "dateTime": "03:56:00", + "value": "1" + }, + { + "dateTime": "03:57:00", + "value": "1" + }, + { + "dateTime": "03:58:00", + "value": "1" + } + ], + "minutesAfterWakeup": 0, + "minutesAsleep": 831, + "minutesAwake": 119, + "minutesToFallAsleep": 11, + "restlessCount": 22, + "restlessDuration": 43, + "startTime": "2014-07-19T11:58:00.000", + "timeInBed": 961 + } + ], + "summary": { + "totalMinutesAsleep": 831, + "totalSleepRecords": 1, + "totalTimeInBed": 961 + } +} \ No newline at end of file diff --git a/shim-server/src/test/resources/org/openmhealth/shim/fitbit/mapper/fitbit-get-user-info.json b/shim-server/src/test/resources/org/openmhealth/shim/fitbit/mapper/fitbit-get-user-info.json new file mode 100644 index 00000000..a0cc36b9 --- /dev/null +++ b/shim-server/src/test/resources/org/openmhealth/shim/fitbit/mapper/fitbit-get-user-info.json @@ -0,0 +1,82 @@ +{ + "user": { + "age": 26, + "avatar": "http://static0.fitbit.com/images/profile/defaultProfile_100_female.gif", + "avatar150": "http://static0.fitbit.com/images/profile/defaultProfile_150_female.gif", + "country": "US", + "dateOfBirth": "1989-05-17", + "displayName": "Sarah", + "distanceUnit": "en_US", + "encodedId": "25X23T", + "foodsLocale": "en_US", + "fullName": "Sarah Francis", + "gender": "FEMALE", + "glucoseUnit": "en_US", + "height": 162.60000000000002, + "heightUnit": "en_US", + "locale": "en_US", + "memberSince": "2013-05-11", + "offsetFromUTCMillis": -14400000, + "startDayOfWeek": "MONDAY", + "strideLengthRunning": 86.60000000000001, + "strideLengthWalking": 67.2, + "timezone": "America/New_York", + "topBadges": [ + { + "badgeGradientEndColor": "FFDB01", + "badgeGradientStartColor": "D99123", + "badgeType": "DAILY_STEPS", + "category": "Daily Steps", + "cheers": [], + "dateTime": "2013-07-27", + "description": "25,000 steps in a day", + "earnedMessage": "Congrats on earning your first Classics badge!", + "encodedId": "228TLX", + "image100px": "http://static0.fitbit.com/images/badges_new/100px/badge_daily_steps25k.png", + "image125px": "http://static0.fitbit.com/images/badges_new/125px/badge_daily_steps25k.png", + "image300px": "http://static0.fitbit.com/images/badges_new/300px/badge_daily_steps25k.png", + "image50px": "http://static0.fitbit.com/images/badges_new/badge_daily_steps25k.png", + "image75px": "http://static0.fitbit.com/images/badges_new/75px/badge_daily_steps25k.png", + "marketingDescription": "You've walked 25,000 steps And earned the Classics badge!", + "mobileDescription": "With this impressive fitness feat, you've added a badge to your growing collection. Nice job, you stepping all-star!", + "name": "Classics (25,000 steps in a day)", + "shareImage640px": "http://static0.fitbit.com/images/badges_new/386px/shareLocalized/en_US/badge_daily_steps25k.png", + "shareText": "I took 25,000 steps and earned the Classics badge! #Fitbit", + "shortDescription": "25,000 steps", + "shortName": "Classics", + "timesAchieved": 1, + "value": 25000 + }, + { + "badgeGradientEndColor": "42C401", + "badgeGradientStartColor": "007D3C", + "badgeType": "LIFETIME_DISTANCE", + "category": "Lifetime Distance", + "cheers": [], + "dateTime": "2015-01-15", + "description": "736 lifetime miles", + "earnedMessage": "Whoa! You've earned the Italy badge!", + "encodedId": "22B8MC", + "image100px": "http://static0.fitbit.com/images/badges_new/100px/badge_lifetime_miles736.png", + "image125px": "http://static0.fitbit.com/images/badges_new/125px/badge_lifetime_miles736.png", + "image300px": "http://static0.fitbit.com/images/badges_new/300px/badge_lifetime_miles736.png", + "image50px": "http://static0.fitbit.com/images/badges_new/badge_lifetime_miles736.png", + "image75px": "http://static0.fitbit.com/images/badges_new/75px/badge_lifetime_miles736.png", + "marketingDescription": "By reaching 736 lifetime miles, you've earned the Italy badge!", + "mobileDescription": "By walking the entire length of Italy, you've stepped your way to another collosal achievement!", + "name": "Italy (736 lifetime miles)", + "shareImage640px": "http://static0.fitbit.com/images/badges_new/386px/shareLocalized/en_US/badge_lifetime_miles736.png", + "shareText": "I covered 736 miles with my #Fitbit and earned the Italy badge.", + "shortDescription": "736 miles", + "shortName": "Italy", + "timesAchieved": 1, + "unit": "MILES", + "value": 736 + } + ], + "waterUnit": "en_US", + "waterUnitName": "fl oz", + "weight": 55.4, + "weightUnit": "en_US" + } +} \ No newline at end of file diff --git a/shim-server/src/test/resources/org/openmhealth/shim/fitbit/mapper/fitbit-time-series-steps.json b/shim-server/src/test/resources/org/openmhealth/shim/fitbit/mapper/fitbit-time-series-steps.json new file mode 100644 index 00000000..2b02b1b3 --- /dev/null +++ b/shim-server/src/test/resources/org/openmhealth/shim/fitbit/mapper/fitbit-time-series-steps.json @@ -0,0 +1,16 @@ +{ + "activities-steps": [ + { + "dateTime": "2015-05-24", + "value": "0" + }, + { + "dateTime": "2015-05-26", + "value": "2170" + }, + { + "dateTime": "2015-05-27", + "value": "3248" + } + ] +} \ No newline at end of file diff --git a/shim-server/src/test/resources/org/openmhealth/shim/googlefit/mapper/googlefit-body-height.json b/shim-server/src/test/resources/org/openmhealth/shim/googlefit/mapper/googlefit-body-height.json new file mode 100644 index 00000000..f81bd85d --- /dev/null +++ b/shim-server/src/test/resources/org/openmhealth/shim/googlefit/mapper/googlefit-body-height.json @@ -0,0 +1,31 @@ +{ + "minStartTimeNs": "1436260400000000000", + "maxEndTimeNs": "1436469400000000000", + "dataSourceId": "derived:com.google.height:com.google.android.gms:merge_height", + "point": [ + { + "startTimeNanos": "1436325426030000000", + "endTimeNanos": "1436325426030000000", + "dataTypeName": "com.google.height", + "originDataSourceId": "raw:com.google.height:com.google.android.apps.fitness:user_input", + "value": [ + { + "fpVal": 1.8287990093231201 + } + ], + "modifiedTimeMillis": "1436325424542" + }, + { + "startTimeNanos": "1436366637544000000", + "endTimeNanos": "1436366638545000000", + "dataTypeName": "com.google.height", + "originDataSourceId": "raw:com.google.height:com.google.android.apps.fitness:user_input", + "value": [ + { + "fpVal": 1.828800082206726 + } + ], + "modifiedTimeMillis": "1436370178573" + } + ] +} \ No newline at end of file diff --git a/shim-server/src/test/resources/org/openmhealth/shim/googlefit/mapper/googlefit-body-weight.json b/shim-server/src/test/resources/org/openmhealth/shim/googlefit/mapper/googlefit-body-weight.json new file mode 100644 index 00000000..5b3471db --- /dev/null +++ b/shim-server/src/test/resources/org/openmhealth/shim/googlefit/mapper/googlefit-body-weight.json @@ -0,0 +1,43 @@ +{ + "minStartTimeNs": "1420099200000000000", + "maxEndTimeNs": "1436469400000000000", + "dataSourceId": "derived:com.google.weight:com.google.android.gms:merge_weight", + "point": [ + { + "startTimeNanos": "1423785600000000000", + "endTimeNanos": "1423785600000000000", + "dataTypeName": "com.google.weight", + "originDataSourceId": "raw:com.google.weight:com.fatsecret.android:", + "value": [ + { + "fpVal": 72.0999984741211 + } + ], + "modifiedTimeMillis": "1423945751811" + }, + { + "startTimeNanos": "1424192233313000000", + "endTimeNanos": "1424192233313000000", + "dataTypeName": "com.google.weight", + "originDataSourceId": "raw:com.google.weight:com.wsl.noom:", + "value": [ + { + "fpVal": 72 + } + ], + "modifiedTimeMillis": "1424192280895" + }, + { + "startTimeNanos": "1436325420000000000", + "endTimeNanos": "1436325430020000000", + "dataTypeName": "com.google.weight", + "originDataSourceId": "raw:com.google.weight:com.google.android.apps.fitness:user_input", + "value": [ + { + "fpVal": 75.75070190429688 + } + ], + "modifiedTimeMillis": "1436325433427" + } + ] +} diff --git a/shim-server/src/test/resources/org/openmhealth/shim/googlefit/mapper/googlefit-calories-burned.json b/shim-server/src/test/resources/org/openmhealth/shim/googlefit/mapper/googlefit-calories-burned.json new file mode 100644 index 00000000..d4eb67a1 --- /dev/null +++ b/shim-server/src/test/resources/org/openmhealth/shim/googlefit/mapper/googlefit-calories-burned.json @@ -0,0 +1,43 @@ +{ + "minStartTimeNs": "1436260400000000000", + "maxEndTimeNs": "1436369400000000000", + "dataSourceId": "derived:com.google.calories.expended:com.google.android.gms:merge_calories_expended", + "point": [ + { + "startTimeNanos": "1436275800000000000", + "endTimeNanos": "1436277600000000000", + "dataTypeName": "com.google.calories.expended", + "originDataSourceId": "raw:com.google.calories.expended:com.google.android.apps.fitness:user_input", + "value": [ + { + "fpVal": 200.0 + } + ], + "modifiedTimeMillis": "1436325507434" + }, + { + "startTimeNanos": "1436277600000000000", + "endTimeNanos": "1436364000000000000", + "dataTypeName": "com.google.calories.expended", + "originDataSourceId": "derived:com.google.calories.expended:com.google.android.gms:from_bmr", + "value": [ + { + "fpVal": 1672.50634765625 + } + ], + "modifiedTimeMillis": "1436398592117" + }, + { + "startTimeNanos": "1436366629730000000", + "endTimeNanos": "1436366847809000000", + "dataTypeName": "com.google.calories.expended", + "originDataSourceId": "derived:com.google.calories.expended:com.google.android.gms:from_activities", + "value": [ + { + "fpVal": 4.221510410308838 + } + ], + "modifiedTimeMillis": "1436370179985" + } + ] +} diff --git a/shim-server/src/test/resources/org/openmhealth/shim/googlefit/mapper/googlefit-empty.json b/shim-server/src/test/resources/org/openmhealth/shim/googlefit/mapper/googlefit-empty.json new file mode 100644 index 00000000..8951f0db --- /dev/null +++ b/shim-server/src/test/resources/org/openmhealth/shim/googlefit/mapper/googlefit-empty.json @@ -0,0 +1,5 @@ +{ + "minStartTimeNs": "1426367600000000000", + "maxEndTimeNs": "1426886000000000000", + "dataSourceId": "derived:com.google.step_count.delta:com.google.android.gms:estimated_steps" +} \ No newline at end of file diff --git a/shim-server/src/test/resources/org/openmhealth/shim/googlefit/mapper/googlefit-heart-rate.json b/shim-server/src/test/resources/org/openmhealth/shim/googlefit/mapper/googlefit-heart-rate.json new file mode 100644 index 00000000..479d26f8 --- /dev/null +++ b/shim-server/src/test/resources/org/openmhealth/shim/googlefit/mapper/googlefit-heart-rate.json @@ -0,0 +1,31 @@ +{ + "minStartTimeNs": "1420099200000000000", + "maxEndTimeNs": "1425110400000000000", + "dataSourceId": "derived:com.google.heart_rate.bpm:com.google.android.gms:merge_heart_rate_bpm", + "point": [ + { + "startTimeNanos": "1422632268186000000", + "endTimeNanos": "1422632268186000000", + "dataTypeName": "com.google.heart_rate.bpm", + "originDataSourceId": "raw:com.google.heart_rate.bpm:com.azumio.instantheartrate.full:", + "value": [ + { + "fpVal": 54 + } + ], + "modifiedTimeMillis": "1423721121864" + }, + { + "startTimeNanos": "1436538872914000000", + "endTimeNanos": "1436538873915000000", + "dataTypeName": "com.google.heart_rate.bpm", + "originDataSourceId": "raw:com.google.heart_rate.bpm:si.modula.android.instantheartrate:", + "value": [ + { + "fpVal": 58.0 + } + ], + "modifiedTimeMillis": "1436539405395" + } + ] +} diff --git a/shim-server/src/test/resources/org/openmhealth/shim/googlefit/mapper/googlefit-physical-activity.json b/shim-server/src/test/resources/org/openmhealth/shim/googlefit/mapper/googlefit-physical-activity.json new file mode 100644 index 00000000..bab680b3 --- /dev/null +++ b/shim-server/src/test/resources/org/openmhealth/shim/googlefit/mapper/googlefit-physical-activity.json @@ -0,0 +1,44 @@ +{ + "minStartTimeNs": "1420099200000000000", + "maxEndTimeNs": "1436277600000000000", + "dataSourceId": "derived:com.google.activity.segment:com.google.android.gms:merge_activity_segments", + "point": [ + { + "startTimeNanos": "1420150917000000000", + "endTimeNanos": "1420154989000000000", + "dataTypeName": "com.google.activity.segment", + "originDataSourceId": "derived:com.google.activity.segment:com.strava:session_activity_segment", + "value": [ + { + "intVal": 7 + } + ], + "modifiedTimeMillis": "1423270946348" + }, + { + "startTimeNanos": "1420417649151000000", + "endTimeNanos": "1420417841151000000", + "dataTypeName": "com.google.activity.segment", + "originDataSourceId": "derived:com.google.activity.segment:com.mapmyrun.android2:session_activity_segment", + "value": [ + { + "intVal": 9 + } + ], + "modifiedTimeMillis": "1420417997203" + }, + { + "startTimeNanos": "1436275800000000000", + "endTimeNanos": "1436277600000000000", + "dataTypeName": "com.google.activity.segment", + "originDataSourceId": "raw:com.google.activity.segment:com.google.android.apps.fitness:user_input", + "value": [ + { + "intVal": 8 + } + ], + "modifiedTimeMillis": "1436325507606" + } + + ] +} diff --git a/shim-server/src/test/resources/org/openmhealth/shim/googlefit/mapper/googlefit-sleep-activity.json b/shim-server/src/test/resources/org/openmhealth/shim/googlefit/mapper/googlefit-sleep-activity.json new file mode 100644 index 00000000..cb7446f7 --- /dev/null +++ b/shim-server/src/test/resources/org/openmhealth/shim/googlefit/mapper/googlefit-sleep-activity.json @@ -0,0 +1,20 @@ +{ + "minStartTimeNs": "1420099200000000000", + "maxEndTimeNs": "1436277600000000000", + "dataSourceId": "derived:com.google.activity.segment:com.google.android.gms:merge_activity_segments", + "point": [ + { + "startTimeNanos": "1436275800000000000", + "endTimeNanos": "1436277600000000000", + "dataTypeName": "com.google.activity.segment", + "originDataSourceId": "raw:com.google.activity.segment:com.google.android.apps.fitness:user_input", + "value": [ + { + "intVal": 72 + } + ], + "modifiedTimeMillis": "1436325507606" + } + + ] +} diff --git a/shim-server/src/test/resources/org/openmhealth/shim/googlefit/mapper/googlefit-stationary-activity.json b/shim-server/src/test/resources/org/openmhealth/shim/googlefit/mapper/googlefit-stationary-activity.json new file mode 100644 index 00000000..21627ff5 --- /dev/null +++ b/shim-server/src/test/resources/org/openmhealth/shim/googlefit/mapper/googlefit-stationary-activity.json @@ -0,0 +1,20 @@ +{ + "minStartTimeNs": "1420099200000000000", + "maxEndTimeNs": "1436277600000000000", + "dataSourceId": "derived:com.google.activity.segment:com.google.android.gms:merge_activity_segments", + "point": [ + { + "startTimeNanos": "1436275800000000000", + "endTimeNanos": "1436277600000000000", + "dataTypeName": "com.google.activity.segment", + "originDataSourceId": "raw:com.google.activity.segment:com.google.android.apps.fitness:user_input", + "value": [ + { + "intVal": 3 + } + ], + "modifiedTimeMillis": "1436325507606" + } + + ] +} diff --git a/shim-server/src/test/resources/org/openmhealth/shim/googlefit/mapper/googlefit-step-count.json b/shim-server/src/test/resources/org/openmhealth/shim/googlefit/mapper/googlefit-step-count.json new file mode 100644 index 00000000..34810157 --- /dev/null +++ b/shim-server/src/test/resources/org/openmhealth/shim/googlefit/mapper/googlefit-step-count.json @@ -0,0 +1,55 @@ +{ + "minStartTimeNs": "1420099200000000000", + "maxEndTimeNs": "1436566038006058105", + "dataSourceId": "derived:com.google.step_count.delta:com.google.android.gms:merge_step_deltas", + "point": [ + { + "startTimeNanos": "1422917379811000000", + "endTimeNanos": "1422919520811000000", + "dataTypeName": "com.google.step_count.delta", + "originDataSourceId": "derived:com.google.step_count.delta:com.nike.plusgps:", + "value": [ + { + "intVal": 4146 + } + ], + "modifiedTimeMillis": "1422919826297" + }, + { + "startTimeNanos": "1436558400000000000", + "endTimeNanos": "1436560200000000000", + "dataTypeName": "com.google.step_count.delta", + "originDataSourceId": "raw:com.google.step_count.delta:com.google.android.apps.fitness:user_input", + "value": [ + { + "intVal": 0 + } + ], + "modifiedTimeMillis": "1436564377147" + }, + { + "startTimeNanos": "1436565497687316406", + "endTimeNanos": "1436565557687316406", + "dataTypeName": "com.google.step_count.delta", + "originDataSourceId": "derived:com.google.step_count.cumulative:com.google.android.gms:samsung:Galaxy Nexus:32b1bd9e:soft_step_counter", + "value": [ + { + "intVal": 17 + } + ], + "modifiedTimeMillis": "1436593407066" + }, + { + "startTimeNanos": "1436565921162000000", + "endTimeNanos": "1436566038006058105", + "dataTypeName": "com.google.step_count.delta", + "originDataSourceId": "raw:com.google.step_count.delta:com.google.android.apps.fitness:user_input", + "value": [ + { + "intVal": 184 + } + ], + "modifiedTimeMillis": "1436593407066" + } + ] +} diff --git a/shim-server/src/test/resources/org/openmhealth/shim/jawbone/mapper/jawbone-body-events.json b/shim-server/src/test/resources/org/openmhealth/shim/jawbone/mapper/jawbone-body-events.json new file mode 100644 index 00000000..85443756 --- /dev/null +++ b/shim-server/src/test/resources/org/openmhealth/shim/jawbone/mapper/jawbone-body-events.json @@ -0,0 +1,70 @@ +{ + "data": { + "items": [ + { + "bmi": null, + "body_fat": null, + "date": 20150811, + "details": { + "tz": "GMT-0600" + }, + "image": "", + "lean_mass": null, + "note": "First weight", + "shared": false, + "time_created": 1439354240, + "time_updated": 1439354240, + "title": null, + "type": "body", + "waistline": null, + "weight": 86.0010436535, + "xid": "QkfTizSpRdubVXHaj3iZk6Vd3qqdl_RJ" + }, + { + "bmi": 24, + "body_fat": null, + "date": 20150811, + "details": { + "tz": "GMT-0600" + }, + "image": "", + "lean_mass": null, + "note": null, + "shared": true, + "time_created": 1439354238, + "time_updated": 1439454238, + "title": null, + "type": "body", + "waistline": null, + "weight": 86.5010436535, + "xid": "QkfTizSpRdukQY3ns4PYbkucZTM5yPMg" + }, + { + "bmi": 25.2, + "body_fat": null, + "date": 20150806, + "details": { + "tz": "GMT-0600" + }, + "image": "", + "lean_mass": null, + "note": null, + "shared": false, + "time_created": 1438925817, + "time_updated": 1438925817, + "title": null, + "type": "body", + "waistline": null, + "weight": null, + "xid": "QkfTizSpRdt6MGLRxULIlVTscmwD_cPJ" + } + ], + "size": 3 + }, + "meta": { + "code": 200, + "message": "OK", + "time": 1439865418, + "user_xid": "VB0mNZWqiOUDWkkl72vgRQ" + } +} \ No newline at end of file diff --git a/shim-server/src/test/resources/org/openmhealth/shim/jawbone/mapper/jawbone-empty.json b/shim-server/src/test/resources/org/openmhealth/shim/jawbone/mapper/jawbone-empty.json new file mode 100644 index 00000000..d8f89e00 --- /dev/null +++ b/shim-server/src/test/resources/org/openmhealth/shim/jawbone/mapper/jawbone-empty.json @@ -0,0 +1,12 @@ +{ + "meta": { + "user_xid": "VB0mNZWqiOWj26_3mxlZjg", + "message": "OK", + "code": 200, + "time": 1441209212 + }, + "data": { + "items": [], + "size": 0 + } +} \ No newline at end of file diff --git a/shim-server/src/test/resources/org/openmhealth/shim/jawbone/mapper/jawbone-heartrates.json b/shim-server/src/test/resources/org/openmhealth/shim/jawbone/mapper/jawbone-heartrates.json new file mode 100644 index 00000000..0189de18 --- /dev/null +++ b/shim-server/src/test/resources/org/openmhealth/shim/jawbone/mapper/jawbone-heartrates.json @@ -0,0 +1,31 @@ +{ + "meta": { + "user_xid": "6xl39CsoVp2KirfHwVq_Fx", + "message": "OK", + "code": 200, + "time": 1382377526 + }, + "data": { + "items": [ + { + "xid": "40F7_htRRnT8Vo7nRBZO1X", + "title": "My resting heart rate on December 10th, 2014", + "time_created": 1384963500, + "time_updated": 1385049599, + "date": 20141210, + "place_lat": "37.451572", + "place_lon": "-122.184435", + "place_acc": 10, + "place_name": "", + "resting_heartrate": 55, + "details": { + "tz": "America/Los_Angeles" + } + } + ], + "links": { + "next": "/nudge/api/v.1.1/users/6xl39CsoVp2KirfHwVq_Fx/heartrates?page_token=1384390680" + }, + "size": 1 + } +} \ No newline at end of file diff --git a/shim-server/src/test/resources/org/openmhealth/shim/jawbone/mapper/jawbone-moves.json b/shim-server/src/test/resources/org/openmhealth/shim/jawbone/mapper/jawbone-moves.json new file mode 100644 index 00000000..25a88082 --- /dev/null +++ b/shim-server/src/test/resources/org/openmhealth/shim/jawbone/mapper/jawbone-moves.json @@ -0,0 +1,164 @@ +{ + "data": { + "items": [ + { + "date": 20150810, + "details": { + "active_time": 101, + "bg_calories": 10.9779999852, + "bmr": 1968.47135419, + "bmr_day": 1968.47135419, + "calories": 13.2817996442, + "distance": 170.0, + "hourly_totals": { + "2015081009": { + "active_time": 81, + "calories": 8.90799993277, + "distance": 137.0, + "inactive_time": 900, + "longest_active_time": 81, + "longest_idle_time": 900, + "steps": 159 + }, + "2015081011": { + "active_time": 20, + "calories": 2.07000005245, + "distance": 33.0, + "inactive_time": 2460, + "longest_active_time": 20, + "longest_idle_time": 2460, + "steps": 38 + } + }, + "inactive_time": 13080, + "km": 0.17, + "longest_active": 81, + "longest_idle": 8580, + "steps": 197, + "sunrise": 0, + "sunset": 0, + "tz": "America/Denver", + "tzs": [ + [ + 1439219760, + "America/Denver" + ] + ], + "wo_active_time": 0, + "wo_calories": 0, + "wo_count": 0, + "wo_longest": 0, + "wo_time": 0 + }, + "snapshot_image": "/nudge/image/e/1439354197/QkfTizSpRdvMvnHFctzItGNZMT-1F5vw/nq7c6F3gu84.png", + "time_completed": 1439228580, + "time_created": 1439219760, + "time_updated": 1439867504, + "title": "197 steps", + "type": "move", + "xid": "QkfTizSpRdvMvnHFctzItGNZMT-1F5vw" + }, + { + "date": 20150805, + "details": { + "active_time": 345, + "bg_calories": 28.3100005388, + "bmr": 1962.99790392, + "bmr_day": 1962.99790392, + "calories": 36.17978471, + "distance": 468.0, + "hourly_totals": { + "2015080500": { + "active_time": 336, + "calories": 27.0590004921, + "distance": 451.0, + "inactive_time": 0, + "longest_active_time": 336, + "longest_idle_time": 0, + "steps": 574 + }, + "2015080506": { + "active_time": 9, + "calories": 1.25100004673, + "distance": 17.0, + "inactive_time": 0, + "longest_active_time": 9, + "longest_idle_time": 0, + "steps": 19 + } + }, + "inactive_time": 32400, + "km": 0.468, + "longest_active": 336, + "longest_idle": 32400, + "steps": 593, + "sunrise": 0, + "sunset": 0, + "tz": "America/Denver", + "tzs": [ + [ + 1438747200, + "America/New_York" + ], + [ + 1438778520, + "America/Denver" + ] + ], + "wo_active_time": 0, + "wo_calories": 0, + "wo_count": 0, + "wo_longest": 0, + "wo_time": 0 + }, + "snapshot_image": "/nudge/image/e/1438778750/QkfTizSpRdsdNZPA9R8QASzqKUk1mod1/nq7c6F3gu84.png", + "time_completed": 1438778520, + "time_created": 1438747200, + "time_updated": 1439867504, + "title": "593 steps", + "type": "move", + "xid": "QkfTizSpRdsdNZPA9R8QASzqKUk1mod1" + }, + { + "date": 20150819, + "details": { + "active_time": 3600, + "bg_calories": 0, + "bmr": 1607.97329172, + "bmr_day": 1607.97329172, + "calories": 385.911882249, + "km": 0.0, + "longest_active": 3600, + "longest_idle": 0, + "steps": 0, + "tz": "America/Denver", + "tzs": [ + [ + 1439994003, + -21600 + ] + ], + "wo_active_time": 3600, + "wo_calories": 316.0, + "wo_count": 1, + "wo_longest": 3600, + "wo_time": 3600 + }, + "snapshot_image": "", + "time_completed": 1439994003, + "time_created": 1439990403, + "time_updated": 1440084303, + "title": "0 steps", + "type": "move", + "xid": "95FS5K2xvcTa3RE2Q1JjlqracI4NI38q" + } + ], + "size": 3 + }, + "meta": { + "code": 200, + "message": "OK", + "time": 1439867504, + "user_xid": "VB0mNZWqiOUDWkkl72vgRQ" + } +} \ No newline at end of file diff --git a/shim-server/src/test/resources/org/openmhealth/shim/jawbone/mapper/jawbone-sleeps.json b/shim-server/src/test/resources/org/openmhealth/shim/jawbone/mapper/jawbone-sleeps.json new file mode 100644 index 00000000..d9fd1a2e --- /dev/null +++ b/shim-server/src/test/resources/org/openmhealth/shim/jawbone/mapper/jawbone-sleeps.json @@ -0,0 +1,72 @@ +{ + "data": { + "items": [ + { + "date": 20150805, + "details": { + "asleep_time": 1438751158, + "awake": 1028, + "awake_time": 1438761300, + "awakenings": 0, + "body": 0, + "duration": 11384, + "light": 0, + "mind": 0, + "quality": 47, + "rem": 0, + "smart_alarm_fire": 0, + "sound": 10356, + "sunrise": 0, + "sunset": 0, + "tz": "America/Denver" + }, + "shared": true, + "snapshot_image": "/nudge/image/e/1438768320/QkfTizSpRdsDKwErMhvMqG9VDhpfyDGd/nq7c6F3gu84.png", + "sub_type": 0, + "time_completed": 1438761515, + "time_created": 1438750131, + "time_updated": 1438768320, + "title": "for 2h 52m", + "xid": "QkfTizSpRdsDKwErMhvMqG9VDhpfyDGd" + }, + { + "date": 20150804, + "details": { + "asleep_time": 1438657500, + "awake": 1500, + "awake_time": 1438686900, + "awakenings": 2, + "body": 0, + "duration": 29400, + "light": 14400, + "mind": 0, + "quality": 93, + "rem": 0, + "smart_alarm_fire": 0, + "sound": 13500, + "sunrise": 1438682100, + "sunset": 0, + "tz": "America/New_York" + }, + "shared": false, + "snapshot_image": "/nudge/image/e/1438690256/QkfTizSpRdvIs6MMJbKP6ulqeYwu5c2v/nq7c6F3gu84.png", + "sub_type": 0, + "time_completed": 1438686900, + "time_created": 1438657500, + "time_updated": 1438690256, + "title": "for 7h 45m", + "xid": "QkfTizSpRdvIs6MMJbKP6ulqeYwu5c2v" + } + ], + "links": { + "next": "/nudge/api/v.1.1/users/VB0mNZWqiOUDWkkl72vgRQ/sleeps?page_token=1435672010&limit=10" + }, + "size": 2 + }, + "meta": { + "code": 200, + "message": "OK", + "time": 1439918078, + "user_xid": "VB0mNZWqiOUDWkkl72vgRQ" + } +} \ No newline at end of file diff --git a/shim-server/src/test/resources/org/openmhealth/shim/jawbone/mapper/jawbone-workouts.json b/shim-server/src/test/resources/org/openmhealth/shim/jawbone/mapper/jawbone-workouts.json new file mode 100644 index 00000000..ac9c7763 --- /dev/null +++ b/shim-server/src/test/resources/org/openmhealth/shim/jawbone/mapper/jawbone-workouts.json @@ -0,0 +1,97 @@ +{ + "meta": { + "user_xid": "6xl39CsoVp2KirfHwVq_Fx", + "message": "OK", + "code": 200, + "time": 1386122022 + }, + "data": { + "items": [ + { + "xid": "40F7_htRRnT8Vo7nRBZO1X", + "title": "Run", + "type": "workout", + "sub_type": 2, + "time_created": 1384963500, + "time_updated": 1385049599, + "time_completed": 1385099220, + "date": 20131121, + "place_lat": "37.451572", + "place_lon": "-122.184435", + "place_acc": 10, + "place_name": "Gym", + "route": "/nudge/image/d/3c1b0970533811e39d7f12313d00449a_image.png", + "image": "/nudge/image/d/3c1b0970533811e39d7f12313d00449a_image.png" , + "snapshot_image": "/nudge/image/e/1385107735/40F7_htRRnT8VJ5BRfj-_A/grEGutn_3mE.png", + "details": { + "steps": 5128, + "time": 2460, + "bg_active_time": 2163, + "meters": 5116, + "km": 5.116, + "intensity": 3, + "calories": 691, + "bmr": 56.071321076, + "bg_calories": 448.95380111, + "bmr_calories": 56.071321076, + "tz": "America/Los_Angeles" + } + }, + { + "date": 20150429, + "details": { + "bg_active_time": 0, + "bg_calories": 0, + "bmr": 7.83136234084, + "bmr_calories": 7.83136234084, + "calories": 35.0, + "goal": 0, + "intensity": null, + "km": 1.188, + "meters": 1188, + "place_acc": 0, + "steps": 0, + "time": 343, + "tz": "America/New_York" + }, + "image": "", + "reaction": null, + "snapshot_image": "", + "sub_type": 14, + "time_completed": 1430338027, + "time_created": 1430337684, + "time_updated": 1430338076, + "title": "Bike", + "xid": "SbiOBJjJJk8n2xLpNTMFng12pGRjX-qe" + }, + { + "date": 20150410, + "details": { + "bg_active_time": 0, + "bg_calories": 0, + "bmr": 19.2051012053, + "bmr_calories": 19.2051012053, + "calories": 221.0, + "goal": 0, + "intensity": null, + "km": 2.322, + "meters": 2322, + "place_acc": 0, + "steps": 0, + "time": 841, + "tz": "America/New_York" + }, + "image": "", + "reaction": null, + "snapshot_image": "", + "sub_type": 2, + "time_completed": 1428674413, + "time_created": 1428673572, + "time_updated": 1434680931, + "title": "Run", + "xid": "EPKA7KhdE4mERrw71404A0680Ea6IbfP" + } + ], + "size": 3 + } +} diff --git a/shim-server/src/test/resources/org/openmhealth/shim/misfit/mapper/misfit-detailed-summaries.json b/shim-server/src/test/resources/org/openmhealth/shim/misfit/mapper/misfit-detailed-summaries.json new file mode 100644 index 00000000..5992f540 --- /dev/null +++ b/shim-server/src/test/resources/org/openmhealth/shim/misfit/mapper/misfit-detailed-summaries.json @@ -0,0 +1,28 @@ +{ + "summary": [ + { + "date": "2015-04-13", + "points": 2745.6, + "steps": 26370, + "calories": 961, + "activityCalories": 961, + "distance": 10.9969 + }, + { + "date": "2015-04-14", + "points": 413.6, + "steps": 3782, + "calories": 144.8, + "activityCalories": 144.8, + "distance": 1.654 + }, + { + "date": "2015-04-15", + "points": 1050.8, + "steps": 9180, + "calories": 222.2095, + "activityCalories": 367.8, + "distance": 4.0358 + } + ] +} diff --git a/shim-server/src/test/resources/org/openmhealth/shim/misfit/mapper/misfit-sessions.json b/shim-server/src/test/resources/org/openmhealth/shim/misfit/mapper/misfit-sessions.json new file mode 100644 index 00000000..38536f8c --- /dev/null +++ b/shim-server/src/test/resources/org/openmhealth/shim/misfit/mapper/misfit-sessions.json @@ -0,0 +1,34 @@ +{ + "sessions": [ + { + "id": "552eab896c59ae1f7300003e", + "activityType": "Walking", + "startTime": "2015-04-13T11:46:00-07:00", + "duration": 1140, + "points": 228.8, + "steps": 2026, + "calories": 96.8, + "distance": 0.9371 + }, + { + "id": "552edc21472c922550000027", + "activityType": "Walking", + "startTime": "2015-04-15T13:27:00-07:00", + "duration": 420, + "points": 0.8, + "steps": 8, + "calories": 0.3, + "distance": 0.003 + }, + { + "id": "552edc21472c922550000028", + "activityType": "Walking", + "startTime": "2015-04-15T14:08:00-07:00", + "duration": 2340, + "points": 25.6, + "steps": 256, + "calories": 10.8, + "distance": 0.0969 + } + ] +} diff --git a/shim-server/src/test/resources/org/openmhealth/shim/misfit/mapper/misfit-sleeps-detected-and-not.json b/shim-server/src/test/resources/org/openmhealth/shim/misfit/mapper/misfit-sleeps-detected-and-not.json new file mode 100644 index 00000000..d4cbd65c --- /dev/null +++ b/shim-server/src/test/resources/org/openmhealth/shim/misfit/mapper/misfit-sleeps-detected-and-not.json @@ -0,0 +1,183 @@ +{ + "sleeps": [ + { + "id": "54fa13a8440f705a7406844f", + "autoDetected": true, + "startTime": "2015-02-23T21:40:59-05:00", + "duration": 11580, + "sleepDetails": [ + { + "datetime": "2015-02-23T21:40:59-05:00", + "value": 2 + }, + { + "datetime": "2015-02-23T21:59:59-05:00", + "value": 3 + }, + { + "datetime": "2015-02-23T22:09:59-05:00", + "value": 2 + }, + { + "datetime": "2015-02-23T22:15:59-05:00", + "value": 1 + }, + { + "datetime": "2015-02-23T22:20:59-05:00", + "value": 2 + }, + { + "datetime": "2015-02-23T22:26:59-05:00", + "value": 3 + }, + { + "datetime": "2015-02-23T22:31:59-05:00", + "value": 2 + }, + { + "datetime": "2015-02-23T22:53:59-05:00", + "value": 1 + }, + { + "datetime": "2015-02-23T23:12:59-05:00", + "value": 2 + }, + { + "datetime": "2015-02-23T23:18:59-05:00", + "value": 3 + }, + { + "datetime": "2015-02-24T00:02:59-05:00", + "value": 2 + }, + { + "datetime": "2015-02-24T00:21:59-05:00", + "value": 3 + }, + { + "datetime": "2015-02-24T00:49:59-05:00", + "value": 2 + } + ] + }, + { + "id": "54fa13a8440f705a7406844f", + "autoDetected": false, + "startTime": "2015-02-23T21:40:59-05:00", + "duration": 11580, + "sleepDetails": [ + { + "datetime": "2015-02-23T21:40:59-05:00", + "value": 2 + }, + { + "datetime": "2015-02-23T21:59:59-05:00", + "value": 3 + }, + { + "datetime": "2015-02-23T22:09:59-05:00", + "value": 2 + }, + { + "datetime": "2015-02-23T22:15:59-05:00", + "value": 1 + }, + { + "datetime": "2015-02-23T22:20:59-05:00", + "value": 2 + }, + { + "datetime": "2015-02-23T22:26:59-05:00", + "value": 3 + }, + { + "datetime": "2015-02-23T22:31:59-05:00", + "value": 2 + }, + { + "datetime": "2015-02-23T22:53:59-05:00", + "value": 1 + }, + { + "datetime": "2015-02-23T23:12:59-05:00", + "value": 2 + }, + { + "datetime": "2015-02-23T23:18:59-05:00", + "value": 3 + }, + { + "datetime": "2015-02-24T00:02:59-05:00", + "value": 2 + }, + { + "datetime": "2015-02-24T00:21:59-05:00", + "value": 3 + }, + { + "datetime": "2015-02-24T00:49:59-05:00", + "value": 2 + } + ] + }, + { + "id": "54fa13a8440f705a7406844f", + "startTime": "2015-02-23T21:40:59-05:00", + "duration": 11580, + "sleepDetails": [ + { + "datetime": "2015-02-23T21:40:59-05:00", + "value": 2 + }, + { + "datetime": "2015-02-23T21:59:59-05:00", + "value": 3 + }, + { + "datetime": "2015-02-23T22:09:59-05:00", + "value": 2 + }, + { + "datetime": "2015-02-23T22:15:59-05:00", + "value": 1 + }, + { + "datetime": "2015-02-23T22:20:59-05:00", + "value": 2 + }, + { + "datetime": "2015-02-23T22:26:59-05:00", + "value": 3 + }, + { + "datetime": "2015-02-23T22:31:59-05:00", + "value": 2 + }, + { + "datetime": "2015-02-23T22:53:59-05:00", + "value": 1 + }, + { + "datetime": "2015-02-23T23:12:59-05:00", + "value": 2 + }, + { + "datetime": "2015-02-23T23:18:59-05:00", + "value": 3 + }, + { + "datetime": "2015-02-24T00:02:59-05:00", + "value": 2 + }, + { + "datetime": "2015-02-24T00:21:59-05:00", + "value": 3 + }, + { + "datetime": "2015-02-24T00:49:59-05:00", + "value": 2 + } + ] + } + ] +} \ No newline at end of file diff --git a/shim-server/src/test/resources/org/openmhealth/shim/misfit/mapper/misfit-sleeps.json b/shim-server/src/test/resources/org/openmhealth/shim/misfit/mapper/misfit-sleeps.json new file mode 100644 index 00000000..f516ad6b --- /dev/null +++ b/shim-server/src/test/resources/org/openmhealth/shim/misfit/mapper/misfit-sleeps.json @@ -0,0 +1,76 @@ +{ + "sleeps": [ + { + "id": "54fa13a8440f705a7406844f", + "autoDetected": true, + "startTime": "2015-02-23T21:40:59-05:00", + "duration": 11580, + "sleepDetails": [ + { + "datetime": "2015-02-23T21:40:59-05:00", + "value": 2 + }, + { + "datetime": "2015-02-23T21:59:59-05:00", + "value": 3 + }, + { + "datetime": "2015-02-23T22:09:59-05:00", + "value": 2 + }, + { + "datetime": "2015-02-23T22:15:59-05:00", + "value": 1 + }, + { + "datetime": "2015-02-23T22:20:59-05:00", + "value": 2 + }, + { + "datetime": "2015-02-23T22:26:59-05:00", + "value": 3 + }, + { + "datetime": "2015-02-23T22:31:59-05:00", + "value": 2 + }, + { + "datetime": "2015-02-23T22:53:59-05:00", + "value": 1 + }, + { + "datetime": "2015-02-23T23:12:59-05:00", + "value": 2 + }, + { + "datetime": "2015-02-23T23:18:59-05:00", + "value": 3 + }, + { + "datetime": "2015-02-24T00:02:59-05:00", + "value": 2 + }, + { + "datetime": "2015-02-24T00:21:59-05:00", + "value": 3 + }, + { + "datetime": "2015-02-24T00:49:59-05:00", + "value": 2 + } + ] + }, + { + "id": "54fa13a8440f705a7406845f", + "autoDetected": false, + "startTime": "2015-02-24T21:40:59-05:00", + "duration": 2580, + "sleepDetails": [ + { + "datetime": "2015-02-23T21:40:59-05:00", + "value": 2 + } + ] + } + ] +} \ No newline at end of file diff --git a/shim-server/src/test/resources/org/openmhealth/shim/runkeeper/mapper/runkeeper-fitness-activities-no-offset.json b/shim-server/src/test/resources/org/openmhealth/shim/runkeeper/mapper/runkeeper-fitness-activities-no-offset.json new file mode 100644 index 00000000..338a556c --- /dev/null +++ b/shim-server/src/test/resources/org/openmhealth/shim/runkeeper/mapper/runkeeper-fitness-activities-no-offset.json @@ -0,0 +1,17 @@ +{ + "items": [ + { + "duration": 3522, + "start_time": "Sun, 24 Jan 2010 10:00:36", + "total_calories": 747.501013824121, + "tracking_mode": "outdoor", + "total_distance": 10000, + "entry_mode": "Web", + "has_path": false, + "source": "RunKeeper", + "type": "Running", + "uri": "/fitnessActivities/4928696" + } + ], + "size": 1 +} diff --git a/shim-server/src/test/resources/org/openmhealth/shim/runkeeper/mapper/runkeeper-fitness-activities.json b/shim-server/src/test/resources/org/openmhealth/shim/runkeeper/mapper/runkeeper-fitness-activities.json new file mode 100644 index 00000000..9b81e9a5 --- /dev/null +++ b/shim-server/src/test/resources/org/openmhealth/shim/runkeeper/mapper/runkeeper-fitness-activities.json @@ -0,0 +1,30 @@ +{ + "items": [ + { + "utc_offset": 2, + "duration": 4364.74158141667, + "start_time": "Sun, 19 Oct 2014 13:17:27", + "total_calories": 210.796359954334, + "tracking_mode": "outdoor", + "total_distance": 10128.7131871337, + "entry_mode": "API", + "has_path": true, + "source": "RunKeeper", + "type": "Cycling", + "uri": "/fitnessActivities/465161536" + }, + { + "utc_offset": -6, + "duration": 2040, + "start_time": "Fri, 24 Jul 2015 16:02:13", + "tracking_mode": "outdoor", + "total_distance": 12874.752, + "entry_mode": "Web", + "has_path": false, + "source": "RunKeeper", + "type": "Walking", + "uri": "/fitnessActivities/619687228" + } + ], + "size": 2 +} \ No newline at end of file diff --git a/shim-server/src/test/resources/org/openmhealth/shim/withings/mapper/withings-activity-measures.json b/shim-server/src/test/resources/org/openmhealth/shim/withings/mapper/withings-activity-measures.json new file mode 100644 index 00000000..e84e0ca1 --- /dev/null +++ b/shim-server/src/test/resources/org/openmhealth/shim/withings/mapper/withings-activity-measures.json @@ -0,0 +1,51 @@ +{ + "body": { + "activities": [ + { + "calories": 139, + "date": "2015-06-18", + "distance": 2563, + "elevation": 0, + "intense": 0, + "moderate": 540, + "soft": 780, + "steps": 2934, + "timezone": "America/Los_Angeles" + }, + { + "calories": 130, + "date": "2015-06-19", + "distance": 2387, + "elevation": 0, + "intense": 120, + "moderate": 420, + "soft": 600, + "steps": 2600, + "timezone": "America/Los_Angeles" + }, + { + "calories": 241, + "date": "2015-06-20", + "distance": 4754, + "elevation": 0, + "intense": 0, + "moderate": 1140, + "soft": 1980, + "steps": 5458, + "timezone": "America/Los_Angeles" + }, + { + "calories": 99, + "date": "2015-02-21", + "distance": 1666, + "elevation": 0, + "intense": 60, + "moderate": 300, + "soft": 480, + "steps": 1798, + "timezone": "America/Los_Angeles" + } + ] + }, + "status": 0 +} \ No newline at end of file diff --git a/shim-server/src/test/resources/org/openmhealth/shim/withings/mapper/withings-body-measures-only-goal.json b/shim-server/src/test/resources/org/openmhealth/shim/withings/mapper/withings-body-measures-only-goal.json new file mode 100644 index 00000000..1cbd6867 --- /dev/null +++ b/shim-server/src/test/resources/org/openmhealth/shim/withings/mapper/withings-body-measures-only-goal.json @@ -0,0 +1,22 @@ +{ + "body": { + "measuregrps": [ + { + "attrib": 0, + "category": 2, + "date": 1429550036, + "grpid": 347186704, + "measures": [ + { + "type": 1, + "unit": -3, + "value": 74128 + } + ] + } + ], + "timezone": "America/Los_Angeles", + "updatetime": 1435524025 + }, + "status": 0 +} \ No newline at end of file diff --git a/shim-server/src/test/resources/org/openmhealth/shim/withings/mapper/withings-body-measures.json b/shim-server/src/test/resources/org/openmhealth/shim/withings/mapper/withings-body-measures.json new file mode 100644 index 00000000..82a32726 --- /dev/null +++ b/shim-server/src/test/resources/org/openmhealth/shim/withings/mapper/withings-body-measures.json @@ -0,0 +1,105 @@ +{ + "body": { + "measuregrps": [ + { + "attrib": 0, + "category": 1, + "date": 1433052383, + "grpid": 366956482, + "measures": [ + { + "type": 9, + "unit": 0, + "value": 68 + }, + { + "type": 10, + "unit": 0, + "value": 104 + }, + { + "type": 11, + "unit": 0, + "value": 41 + }, + { + "type": 18, + "unit": -3, + "value": -1000 + }, + { + "type": 1, + "unit": -3, + "value": 74126 + } + ] + }, + { + "attrib": 0, + "category": 1, + "date": 1429550036, + "grpid": 347186704, + "measures": [ + { + "type": 1, + "unit": -3, + "value": 74128 + } + ] + }, + { + "attrib": 2, + "category": 1, + "date": 1425313735, + "grpid": 323560022, + "measures": [ + { + "type": 11, + "unit": 0, + "value": 51 + }, + { + "type": 18, + "unit": -3, + "value": -1000 + } + ] + }, + { + "attrib": 2, + "category": 1, + "comment": "a few minutes after a walk", + "date": 1424987837, + "grpid": 321858727, + "measures": [ + { + "type": 11, + "unit": 0, + "value": 47 + }, + { + "type": 18, + "unit": -3, + "value": -1000 + } + ] + }, + { + "attrib": 2, + "category": 1, + "date": 1424719489, + "grpid": 320419189, + "measures": [ + { + "type": 4, + "unit": -3, + "value": 1930 + } + ] + } + ], + "timezone": "America/Los_Angeles", + "updatetime": 1435524025 + }, + "status": 0 +} \ No newline at end of file diff --git a/shim-server/src/test/resources/org/openmhealth/shim/withings/mapper/withings-intraday-activity.json b/shim-server/src/test/resources/org/openmhealth/shim/withings/mapper/withings-intraday-activity.json new file mode 100644 index 00000000..75ef2e78 --- /dev/null +++ b/shim-server/src/test/resources/org/openmhealth/shim/withings/mapper/withings-intraday-activity.json @@ -0,0 +1,41 @@ +{ + "body": { + "series": { + "1434758640": { + "calories": 1, + "distance": 15, + "duration": 60, + "elevation": 0, + "steps": 21 + }, + "1434758700": { + "duration": 1440 + }, + "1434760140": { + "calories": 2, + "distance": 35, + "duration": 60, + "elevation": 0, + "steps": 47 + }, + "1434760200": { + "calories": 1, + "distance": 17, + "duration": 60, + "elevation": 0, + "steps": 20 + }, + "1434760260": { + "duration": 600 + }, + "1434760860": { + "calories": 7, + "distance": 59, + "duration": 60, + "elevation": 0, + "steps": 74 + } + } + }, + "status": 0 +} \ No newline at end of file diff --git a/shim-server/src/test/resources/org/openmhealth/shim/withings/mapper/withings-sleep-measures.json b/shim-server/src/test/resources/org/openmhealth/shim/withings/mapper/withings-sleep-measures.json new file mode 100644 index 00000000..7fd6cbf6 --- /dev/null +++ b/shim-server/src/test/resources/org/openmhealth/shim/withings/mapper/withings-sleep-measures.json @@ -0,0 +1,23 @@ +{ + "status": 0, + "body": { + "series": [ + { + "startdate": 1417550400, + "state": 0, + "enddate": 1417551300 + }, + { + "startdate": 1417551360, + "state": 1, + "enddate": 1417552320 + }, + { + "startdate": 1417552380, + "state": 3, + "enddate": 1417554000 + } + ], + "model": 32 + } +} \ No newline at end of file diff --git a/shim-server/src/test/resources/org/openmhealth/shim/withings/mapper/withings-sleep-summary.json b/shim-server/src/test/resources/org/openmhealth/shim/withings/mapper/withings-sleep-summary.json new file mode 100644 index 00000000..53b3b0a2 --- /dev/null +++ b/shim-server/src/test/resources/org/openmhealth/shim/withings/mapper/withings-sleep-summary.json @@ -0,0 +1,26 @@ +{ + "status": 0, + "body": { + "series": [ + { + "id": 16616514, + "timezone": "Europe/Paris", + "model": 32, + "startdate": 1410521659, + "enddate": 1410542577, + "date": "2014-09-11", + "data": { + "wakeupduration": 1800, + "lightsleepduration": 18540, + "deepsleepduration": 8460, + "remsleepduration": 10460, + "durationtosleep": 420, + "durationtowakeup": 360, + "wakeupcount": 3 + }, + "modified": 1412087110 + } + ], + "more": false + } +} \ No newline at end of file diff --git a/shim-server/src/test/resources/runkeeper-activity.json b/shim-server/src/test/resources/runkeeper-activity.json deleted file mode 100644 index 5a5599ac..00000000 --- a/shim-server/src/test/resources/runkeeper-activity.json +++ /dev/null @@ -1,27 +0,0 @@ -{ - "items": [ - { - "duration": 3600, - "total_distance": 6437.376, - "has_path": false, - "entry_mode": "Web", - "source": "RunKeeper", - "start_time": "Wed, 6 Aug 2014 04:49:00", - "total_calories": 433, - "type": "Rowing", - "uri": "/fitnessActivities/412927691" - }, - { - "duration": 1800, - "total_distance": 578.739946633975, - "has_path": true, - "entry_mode": "Web", - "source": "RunKeeper", - "start_time": "Fri, 1 Aug 2014 06:53:05", - "total_calories": 59, - "type": "Running", - "uri": "/fitnessActivities/404833504" - } - ], - "size": 2 -} \ No newline at end of file diff --git a/shim-server/src/test/resources/withings-intraday-activity.json b/shim-server/src/test/resources/withings-intraday-activity.json new file mode 100644 index 00000000..44f1ac97 --- /dev/null +++ b/shim-server/src/test/resources/withings-intraday-activity.json @@ -0,0 +1,519 @@ +{ + "status": 0, + "body": { + "series": { + "1426550520": { + "steps": 28, + "elevation": 0, + "calories": 2, + "distance": 26, + "duration": 60 + }, + "1426550580": { + "steps": 5, + "elevation": 0, + "calories": 1, + "distance": 3, + "duration": 60 + }, + "1426554900": { + "steps": 5, + "elevation": 0, + "calories": 1, + "distance": 4, + "duration": 60 + }, + "1426555980": { + "steps": 81, + "elevation": 0, + "calories": 3, + "distance": 76, + "duration": 60 + }, + "1426556040": { + "steps": 114, + "elevation": 0, + "calories": 4, + "distance": 107, + "duration": 60 + }, + "1426556100": { + "steps": 33, + "elevation": 0, + "calories": 2, + "distance": 26, + "duration": 60 + }, + "1426556340": { + "steps": 83, + "elevation": 0, + "calories": 3, + "distance": 69, + "duration": 60 + }, + "1426556400": { + "steps": 98, + "elevation": 0, + "calories": 3, + "distance": 83, + "duration": 60 + }, + "1426556460": { + "steps": 89, + "elevation": 0, + "calories": 3, + "distance": 73, + "duration": 60 + }, + "1426556520": { + "steps": 77, + "elevation": 0, + "calories": 3, + "distance": 64, + "duration": 60 + }, + "1426556580": { + "steps": 82, + "elevation": 0, + "calories": 3, + "distance": 68, + "duration": 60 + }, + "1426556640": { + "steps": 95, + "elevation": 0, + "calories": 3, + "distance": 79, + "duration": 60 + }, + "1426556700": { + "steps": 96, + "elevation": 0, + "calories": 3, + "distance": 79, + "duration": 60 + }, + "1426556760": { + "steps": 96, + "elevation": 0, + "calories": 3, + "distance": 80, + "duration": 60 + }, + "1426556820": { + "steps": 94, + "elevation": 0, + "calories": 3, + "distance": 78, + "duration": 60 + }, + "1426556880": { + "steps": 94, + "elevation": 0, + "calories": 3, + "distance": 76, + "duration": 60 + }, + "1426556940": { + "steps": 97, + "elevation": 0, + "calories": 3, + "distance": 80, + "duration": 60 + }, + "1426557000": { + "steps": 95, + "elevation": 0, + "calories": 3, + "distance": 79, + "duration": 60 + }, + "1426557060": { + "steps": 91, + "elevation": 0, + "calories": 3, + "distance": 75, + "duration": 60 + }, + "1426557120": { + "steps": 82, + "elevation": 0, + "calories": 3, + "distance": 66, + "duration": 60 + }, + "1426557180": { + "steps": 99, + "elevation": 0, + "calories": 3, + "distance": 80, + "duration": 60 + }, + "1426557240": { + "steps": 75, + "elevation": 0, + "calories": 2, + "distance": 60, + "duration": 60 + }, + "1426557300": { + "steps": 45, + "elevation": 0, + "calories": 2, + "distance": 34, + "duration": 60 + }, + "1426557360": { + "steps": 22, + "elevation": 0, + "calories": 1, + "distance": 16, + "duration": 60 + }, + "1426557420": { + "steps": 23, + "elevation": 0, + "calories": 1, + "distance": 18, + "duration": 60 + }, + "1426557480": { + "steps": 76, + "elevation": 0, + "calories": 2, + "distance": 58, + "duration": 60 + }, + "1426557540": { + "steps": 80, + "elevation": 0, + "calories": 3, + "distance": 68, + "duration": 60 + }, + "1426557600": { + "steps": 88, + "elevation": 0, + "calories": 3, + "distance": 75, + "duration": 60 + }, + "1426557660": { + "steps": 88, + "elevation": 0, + "calories": 3, + "distance": 72, + "duration": 60 + }, + "1426557720": { + "steps": 79, + "elevation": 0, + "calories": 3, + "distance": 66, + "duration": 60 + }, + "1426557780": { + "steps": 80, + "elevation": 0, + "calories": 3, + "distance": 66, + "duration": 60 + }, + "1426557840": { + "steps": 62, + "elevation": 0, + "calories": 2, + "distance": 52, + "duration": 60 + }, + "1426560420": { + "steps": 5, + "elevation": 0, + "calories": 1, + "distance": 4, + "duration": 60 + }, + "1426560480": { + "steps": 6, + "elevation": 0, + "calories": 1, + "distance": 4, + "duration": 60 + }, + "1426560660": { + "steps": 72, + "elevation": 0, + "calories": 3, + "distance": 61, + "duration": 60 + }, + "1426560720": { + "steps": 90, + "elevation": 0, + "calories": 3, + "distance": 75, + "duration": 60 + }, + "1426566060": { + "steps": 26, + "elevation": 0, + "calories": 1, + "distance": 21, + "duration": 60 + }, + "1426566120": { + "steps": 86, + "elevation": 0, + "calories": 3, + "distance": 73, + "duration": 60 + }, + "1426566180": { + "steps": 41, + "elevation": 0, + "calories": 2, + "distance": 34, + "duration": 60 + }, + "1426568940": { + "steps": 39, + "elevation": 0, + "calories": 2, + "distance": 33, + "duration": 60 + }, + "1426569120": { + "steps": 5, + "elevation": 0, + "calories": 1, + "distance": 4, + "duration": 60 + }, + "1426569360": { + "steps": 17, + "elevation": 0, + "calories": 1, + "distance": 14, + "duration": 60 + }, + "1426570200": { + "steps": 74, + "elevation": 0, + "calories": 3, + "distance": 67, + "duration": 60 + }, + "1426570260": { + "steps": 75, + "elevation": 0, + "calories": 3, + "distance": 63, + "duration": 60 + }, + "1426570320": { + "steps": 91, + "elevation": 0, + "calories": 3, + "distance": 84, + "duration": 60 + }, + "1426570380": { + "steps": 100, + "elevation": 0, + "calories": 4, + "distance": 91, + "duration": 60 + }, + "1426570440": { + "steps": 70, + "elevation": 0, + "calories": 3, + "distance": 64, + "duration": 60 + }, + "1426570500": { + "steps": 82, + "elevation": 0, + "calories": 3, + "distance": 72, + "duration": 60 + }, + "1426570560": { + "steps": 79, + "elevation": 0, + "calories": 3, + "distance": 67, + "duration": 60 + }, + "1426570620": { + "steps": 56, + "elevation": 0, + "calories": 2, + "distance": 50, + "duration": 60 + }, + "1426570680": { + "steps": 41, + "elevation": 0, + "calories": 2, + "distance": 32, + "duration": 60 + }, + "1426570740": { + "steps": 86, + "elevation": 0, + "calories": 3, + "distance": 72, + "duration": 60 + }, + "1426570800": { + "steps": 75, + "elevation": 0, + "calories": 3, + "distance": 64, + "duration": 60 + }, + "1426570860": { + "steps": 81, + "elevation": 0, + "calories": 3, + "distance": 68, + "duration": 60 + }, + "1426570920": { + "steps": 42, + "elevation": 0, + "calories": 2, + "distance": 35, + "duration": 60 + }, + "1426570980": { + "steps": 109, + "elevation": 0, + "calories": 4, + "distance": 97, + "duration": 60 + }, + "1426571040": { + "steps": 104, + "elevation": 0, + "calories": 4, + "distance": 93, + "duration": 60 + }, + "1426571100": { + "steps": 74, + "elevation": 0, + "calories": 3, + "distance": 61, + "duration": 60 + }, + "1426571160": { + "steps": 72, + "elevation": 0, + "calories": 3, + "distance": 60, + "duration": 60 + }, + "1426571220": { + "steps": 87, + "elevation": 0, + "calories": 3, + "distance": 75, + "duration": 60 + }, + "1426571280": { + "steps": 93, + "elevation": 0, + "calories": 3, + "distance": 81, + "duration": 60 + }, + "1426571340": { + "steps": 85, + "elevation": 0, + "calories": 3, + "distance": 70, + "duration": 60 + }, + "1426571400": { + "steps": 82, + "elevation": 0, + "calories": 3, + "distance": 67, + "duration": 60 + }, + "1426571460": { + "steps": 98, + "elevation": 0, + "calories": 3, + "distance": 87, + "duration": 60 + }, + "1426571520": { + "steps": 97, + "elevation": 0, + "calories": 3, + "distance": 82, + "duration": 60 + }, + "1426571580": { + "steps": 71, + "elevation": 0, + "calories": 3, + "distance": 62, + "duration": 60 + }, + "1426571640": { + "steps": 6, + "elevation": 0, + "calories": 1, + "distance": 4, + "duration": 60 + }, + "1426571940": { + "steps": 5, + "elevation": 0, + "calories": 1, + "distance": 5, + "duration": 60 + }, + "1426572660": { + "steps": 6, + "elevation": 0, + "calories": 1, + "distance": 6, + "duration": 60 + }, + "1426572720": { + "steps": 8, + "elevation": 0, + "calories": 1, + "distance": 4, + "duration": 60 + }, + "1426604760": { + "steps": 14, + "elevation": 0, + "calories": 1, + "distance": 12, + "duration": 60 + }, + "1426550640": {"duration": 4260}, + "1426554960": {"duration": 300}, + "1426555260": {"duration": 720}, + "1426556160": {"duration": 180}, + "1426557900": {"duration": 2520}, + "1426560540": {"duration": 120}, + "1426560780": {"duration": 5280}, + "1426566240": {"duration": 2700}, + "1426569000": {"duration": 120}, + "1426569180": {"duration": 180}, + "1426569420": {"duration": 780}, + "1426571700": {"duration": 240}, + "1426572000": {"duration": 660}, + "1426572780": {"duration": 31980}, + "1426604820": {"duration": 32460} + } + } +} \ No newline at end of file diff --git a/update-compose-files.sh b/update-compose-files.sh new file mode 100755 index 00000000..dd4e3334 --- /dev/null +++ b/update-compose-files.sh @@ -0,0 +1,24 @@ +#!/bin/bash + +BASEDIR=`pwd` + +# check dependencies +if ! hash "docker-machine" 2>/dev/null; +then + echo "docker-machine can't be found" + exit 1 +fi + +if [[ -z "$DOCKER_MACHINE_NAME" ]]; then + echo "DOCKER_MACHINE_NAME environment variable isn't set. Have you run docker-machine env?" + exit 1 +fi + +# update the compose files +ip=$(docker-machine ip $DOCKER_MACHINE_NAME) +echo Updating the Docker Compose files to match your environment... +cd ${BASEDIR} #CMD +sed -i ".bak" -e "s,.*OPENMHEALTH_SHIM_SERVER_CALLBACKURLBASE.*, OPENMHEALTH_SHIM_SERVER_CALLBACKURLBASE: http://${ip}:8083,g" docker-compose.yml +sed -i ".bak" -e "s,.*OPENMHEALTH_SHIM_SERVER_CALLBACKURLBASE.*, OPENMHEALTH_SHIM_SERVER_CALLBACKURLBASE: http://${ip}:8083,g" docker-compose-build.yml +#CMD modify the URL in docker-compose.yml and docker-compose-build.yml to point to the IP of your Docker host +