Java tutorial
/* * 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)); } }