org.openmhealth.shim.jawbone.mapper.JawboneDataPointMapper.java Source code

Java tutorial

Introduction

Here is the source code for org.openmhealth.shim.jawbone.mapper.JawboneDataPointMapper.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.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 data points contained in an array to {@link
 * Measure} objects.
 *
 * @author Chris Schaefbauer
 * @author Emerson Farrugia
 */
public abstract class JawboneDataPointMapper<T extends Measure> implements JsonNodeDataPointMapper<T> {

    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<T> 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<DataPoint<T>> asDataPoints(List<JsonNode> 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<DataPoint<T>> dataPoints = new ArrayList<>();

        for (JsonNode itemNode : itemsNode) {
            Optional<T> 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<Long> optionalStartTime = asOptionalLong(listEntryNode, "time_created");
        Optional<Long> 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<JsonNode> optionalTimeZonesNode = asOptionalNode(listEntryNode, "details.tzs");
        Optional<JsonNode> 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));
    }
}