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.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 <T> the measure to map to * @author Chris Schaefbauer * @author Emerson Farrugia * @see <a href="http://oauth.withings.com/api/doc#api-Measure-get_measure">Body Measures API documentation</a> */ public abstract class WithingsBodyMeasureDataPointMapper<T extends Measure> extends WithingsDataPointMapper<T> { private static final Logger logger = getLogger(WithingsBodyMeasureDataPointMapper.class); @Override public List<DataPoint<T>> asDataPoints(List<JsonNode> 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<DataPoint<T>> 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<T, ?> measureBuilder = newMeasureBuilder(measuresNode).orElse(null); if (measureBuilder == null) { continue; } Optional<Long> dateTimeInEpochSeconds = asOptionalLong(measureGroupNode, "date"); if (dateTimeInEpochSeconds.isPresent()) { Instant dateTimeInstant = Instant.ofEpochSecond(dateTimeInEpochSeconds.get()); measureBuilder.setEffectiveTimeFrame(OffsetDateTime.ofInstant(dateTimeInstant, UTC)); } Optional<String> userComment = asOptionalString(measureGroupNode, "comment"); if (userComment.isPresent()) { measureBuilder.setUserNotes(userComment.get()); } T measure = measureBuilder.build(); Optional<Long> 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<WithingsMeasureGroupAttribution> getMeasureGroupAttribution(JsonNode measureGroupNode) { Optional<Integer> attributionValue = asOptionalInteger(measureGroupNode, "attrib"); if (attributionValue.isPresent()) { Optional<WithingsMeasureGroupAttribution> 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<Measure.Builder<T, ?>> 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<BigDecimal> getValueForMeasureType(JsonNode measuresNode, WithingsBodyMeasureType bodyMeasureType) { List<BigDecimal> 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)); } }