org.openmhealth.shim.withings.mapper.WithingsBodyMeasureDataPointMapper.java Source code

Java tutorial

Introduction

Here is the source code for org.openmhealth.shim.withings.mapper.WithingsBodyMeasureDataPointMapper.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.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));
    }
}