All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.yahoo.bard.webservice.web.ResponseData Maven / Gradle / Ivy

Go to download

Fili web service library provides core capabilities for RESTful aggregation navigation, query planning and metadata

There is a newer version: 1.1.13
Show newest version
// Copyright 2017 Oath Inc.
// Licensed under the terms of the Apache license. Please see LICENSE.md file distributed with this work for terms.
package com.yahoo.bard.webservice.web;

import com.yahoo.bard.webservice.data.Result;
import com.yahoo.bard.webservice.data.ResultSet;
import com.yahoo.bard.webservice.data.dimension.Dimension;
import com.yahoo.bard.webservice.data.dimension.DimensionColumn;
import com.yahoo.bard.webservice.data.dimension.DimensionField;
import com.yahoo.bard.webservice.data.dimension.DimensionRow;
import com.yahoo.bard.webservice.data.metric.LogicalMetric;
import com.yahoo.bard.webservice.data.metric.MetricColumn;
import com.yahoo.bard.webservice.data.time.TimeDimension;
import com.yahoo.bard.webservice.util.DateTimeFormatterFactory;
import com.yahoo.bard.webservice.util.DateTimeUtils;
import com.yahoo.bard.webservice.util.Pagination;
import com.yahoo.bard.webservice.util.SimplifiedIntervalList;
import com.yahoo.bard.webservice.util.StreamUtils;
import com.yahoo.bard.webservice.web.apirequest.DataApiRequest;

import org.joda.time.Interval;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.net.URI;
import java.util.Collection;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.function.Function;
import java.util.stream.Collectors;
import java.util.stream.Stream;

/**
 * ResponseData class. A bag of metadata that may be needed by the `ResponseWriter` to serialize Fili's response,
 * including the `ResultSet` itself, along with a collection of methods to help `ResponseWriters` perform
 * serialization.
 */
public class ResponseData {

    protected static final Logger LOG = LoggerFactory.getLogger(ResponseData.class);
    protected static final Map> DIMENSION_FIELD_COLUMN_NAMES = new HashMap<>();

    protected final ResultSet resultSet;
    protected final LinkedHashSet apiMetricColumns;
    protected final LinkedHashMap> requestedApiDimensionFields;
    protected final SimplifiedIntervalList missingIntervals;
    protected final SimplifiedIntervalList volatileIntervals;
    protected final Pagination pagination;
    protected final Map paginationLinks;

    /**
     * Constructor.
     *
     * @param resultSet  ResultSet to turn into response
     * @param apiMetricColumnNames  The names of the logical metrics requested
     * @param requestedApiDimensionFields  The fields for each dimension that should be shown in the response
     * @param missingIntervals  intervals over which partial data exists
     * @param volatileIntervals  intervals over which data is understood as 'best-to-date'
     * @param pagination  The object containing the pagination information. Null if we are not paginating.
     * @param paginationLinks  A mapping from link names to links to be added to the end of the JSON response.
     */
    public ResponseData(
            ResultSet resultSet,
            LinkedHashSet apiMetricColumnNames,
            LinkedHashMap> requestedApiDimensionFields,
            SimplifiedIntervalList missingIntervals,
            SimplifiedIntervalList volatileIntervals,
            Pagination pagination,
            Map paginationLinks
    ) {
        this.resultSet = resultSet;
        this.apiMetricColumns = generateApiMetricColumns(apiMetricColumnNames);
        this.requestedApiDimensionFields = requestedApiDimensionFields;
        this.missingIntervals = missingIntervals;
        this.volatileIntervals = volatileIntervals;
        this.pagination = pagination;
        this.paginationLinks = paginationLinks;

        LOG.trace("Initialized with ResultSet: {}", this.resultSet);
    }

    /**
     * Constructor.
     *
     * @param resultSet  ResultSet to turn into response
     * @param apiRequest  API Request to get the metric columns from
     * @param missingIntervals  intervals over which partial data exists
     * @param volatileIntervals  intervals over which data is understood as 'best-to-date'
     * @param pagination  The object containing the pagination information. Null if we are not paginating.
     * @param paginationLinks  A mapping from link names to links to be added to the end of the JSON response.
     *
     * @deprecated  All the values needed to build a Response should be passed explicitly instead of relying on the
     * DataApiRequest
     */
    @Deprecated
    public ResponseData(
            ResultSet resultSet,
            DataApiRequest apiRequest,
            SimplifiedIntervalList missingIntervals,
            SimplifiedIntervalList volatileIntervals,
            Pagination pagination,
            Map paginationLinks
    ) {
        this(
                resultSet,
                apiRequest.getLogicalMetrics().stream()
                        .map(LogicalMetric::getName)
                        .collect(Collectors.toCollection(LinkedHashSet::new)),
                apiRequest.getDimensionFields(),
                missingIntervals,
                volatileIntervals,
                pagination,
                paginationLinks
        );
    }

    public ResultSet getResultSet() {
        return resultSet;
    }

    public SimplifiedIntervalList getMissingIntervals() {
        return missingIntervals;
    }

    public SimplifiedIntervalList getVolatileIntervals() {
        return volatileIntervals;
    }

    public Pagination getPagination() {
        return pagination;
    }

    public Map getPaginationLinks() {
        return paginationLinks;
    }

    public LinkedHashMap> getRequestedApiDimensionFields() {
        return requestedApiDimensionFields;
    }

    public LinkedHashSet getApiMetricColumns() {
        return apiMetricColumns;
    }

    /**
     * Builds a set of only those metric columns which correspond to the metrics requested in the API.
     *
     * @param apiMetricColumnNames  Set of Metric names extracted from the requested api metrics
     *
     * @return set of metric columns
     */
    protected LinkedHashSet generateApiMetricColumns(Set apiMetricColumnNames) {
        // Get the metric columns from the schema
        Map metricColumnMap = resultSet.getSchema().getColumns(MetricColumn.class).stream()
                .collect(StreamUtils.toLinkedDictionary(MetricColumn::getName));

        // Select only api metrics from resultSet
        return apiMetricColumnNames.stream()
                .map(metricColumnMap::get)
                .collect(Collectors.toCollection(LinkedHashSet::new));
    }

    /**
     * Build the headers for the dimension columns.
     *
     * @param entry  Entry to base the columns on.
     *
     * @return the headers as a Stream
     */
    public Stream generateDimensionColumnHeaders(Map.Entry> entry) {
        if (entry.getValue().isEmpty()) {
            return Stream.empty();
        }
        return entry.getValue().stream().map(dimField -> getDimensionColumnName(entry.getKey(), dimField));
    }

    /**
     * Builds map of result row from a result.
     *
     * @param result  The result to process
     *
     * @return map of result row
     */
    public Map buildResultRow(Result result) {
        Map outputRow = new LinkedHashMap<>();
        String timeStamp = result.getTimestamp(DateTimeFormatterFactory.getOutputFormatter());
        outputRow.put(TimeDimension.TIME_DIMENSION_NAME, timeStamp);

        // TODO change this to loop on requested fields instead
        Set requestedDimensions = requestedApiDimensionFields.keySet();

        // Loop through the Map and format it to dimension : dimensionRowDesc
        Map resultRowsByDimension = result.getDimensionRows().entrySet().stream()
                .collect(Collectors.toMap(e -> e.getKey().getDimension(), Entry::getValue));

        for (Dimension dim : requestedDimensions) {
            Set fields = requestedApiDimensionFields.get(dim);
            DimensionRow drow = resultRowsByDimension.get(dim);
            if (drow != null) {
                for (DimensionField field : fields) {
                    outputRow.put(getDimensionColumnName(dim, field), drow.get(field));
                }
            } else {
                for (DimensionField field : fields) {
                    outputRow.put(getDimensionColumnName(dim, field), null);
                }
            }
        }

        // Loop through the Map and format it to a metricColumnName: metricValue map
        for (MetricColumn apiMetricColumn : apiMetricColumns) {
            outputRow.put(apiMetricColumn.getName(), result.getMetricValue(apiMetricColumn));
        }
        return outputRow;
    }

    /**
     * Builds map of result row from a result and loads the dimension rows into the sidecar map.
     *
     * @param result  The result to process
     * @param sidecars  Map of sidecar data (dimension rows in the result)
     *
     * @return map of result row
     */
    public Map buildResultRowWithSidecars(
            Result result,
            Map>> sidecars
    ) {

        Map row = new LinkedHashMap<>();
        row.put(
                TimeDimension.TIME_DIMENSION_NAME,
                result.getTimestamp(DateTimeFormatterFactory.getOutputFormatter())
        );

        // Loop through the Map and format it to dimensionColumnName : dimensionRowKey
        Map dr = result.getDimensionRows();
        for (Entry dimensionColumnEntry : dr.entrySet()) {
            // Get the pieces we need out of the map entry
            Dimension dimension = dimensionColumnEntry.getKey().getDimension();
            DimensionRow dimensionRow = dimensionColumnEntry.getValue();
            LinkedHashSet requestedDimensionFields = requestedApiDimensionFields.get(dimension);

            if (requestedDimensionFields == null || requestedDimensionFields.isEmpty())
            {
                // add sidecar only if at-least one field needs to be shown
                continue;
            }

            // The key field is required
            requestedDimensionFields.add(dimension.getKey());

            Map dimensionFieldToValueMap = requestedDimensionFields.stream()
                    .collect(StreamUtils.toLinkedMap(Function.identity(), dimensionRow::get));

            // Add the dimension row's requested fields to the sidecar map
            sidecars.get(dimension).add(dimensionFieldToValueMap);

            // Put the dimension name and dimension row's key value into the row map
            row.put(dimension.getApiName(), dimensionRow.get(dimension.getKey()));
        }

        // Loop through the Map and format it to a metricColumnName: metricValue map
        for (MetricColumn apiMetricColumn : apiMetricColumns) {
            row.put(apiMetricColumn.getName(), result.getMetricValue(apiMetricColumn));
        }
        return row;
    }

    /**
     * Build a list of interval strings. Format of interval string: yyyy-MM-dd' 'HH:mm:ss/yyyy-MM-dd' 'HH:mm:ss
     *
     * @param intervals  list of intervals to be converted into string
     *
     * @return list of interval strings
     */
    public List buildIntervalStringList(Collection intervals) {
        return intervals.stream()
                .map(it -> DateTimeUtils.intervalToString(it, DateTimeFormatterFactory.getOutputFormatter(), "/"))
                .collect(Collectors.toList());
    }

    /**
     * Retrieve dimension column name from cache, or build it and cache it.
     *
     * @param dimension  The dimension for the column name
     * @param dimensionField  The dimensionField for the column name
     *
     * @return The name for the dimension and column as it will appear in the response document
     */
    public static String getDimensionColumnName(Dimension dimension, DimensionField dimensionField) {
        Map columnNamesForDimensionFields;
        columnNamesForDimensionFields = DIMENSION_FIELD_COLUMN_NAMES.computeIfAbsent(
                dimension,
                (key) -> new ConcurrentHashMap()
        );

        return columnNamesForDimensionFields.computeIfAbsent(
                dimensionField,
                (field) -> field.getName()
                        .isEmpty() ? dimension.getApiName() : dimension.getApiName() + "|" + field.getName()
        );
    }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy