org.openmhealth.shim.googlefit.mapper.GoogleFitDataPointMapper.java Source code

Java tutorial

Introduction

Here is the source code for org.openmhealth.shim.googlefit.mapper.GoogleFitDataPointMapper.java

Source

/*
 * 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<T extends Measure> implements JsonNodeDataPointMapper<T> {

    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<DataPoint<T>> asDataPoints(List<JsonNode> responseNodes) {

        checkNotNull(responseNodes);
        checkArgument(responseNodes.size() == 1, "Only one response should be input to the mapper");

        List<DataPoint<T>> dataPoints = Lists.newArrayList();
        Optional<JsonNode> 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<DataPoint<T>> 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<T> 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<String> startTimeNanosString = asOptionalString(listNode, "startTimeNanos");
        Optional<String> 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";
    }

}