Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Have TablesApiRequest Support Additional Query Parameters #437

Merged
merged 5 commits into from
Aug 2, 2017
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,14 @@ Current

### Added:

- [Have Tables Endpoint Support (but not use) Additional Query Parameters](https://github.com/yahoo/fili/pull/437)
* Make the availability consider the TablesApiRequest by passing it into the getLogicalTableFullView method
* Move auxiliary methods from `DataApiRequest` to `ApiRequest` in order to make them sharable between
`DataApiRequest` and `TableApiRequest`.

- [Fili-security module](https://github.com/yahoo/fili/pull/405)
* Added security module for fili data security filters
* Created `ChainingRequestMapper`, and a set of mappers for gatekeeping on security roles and whitelisting dimension filters.
* Created `ChainingRequestMapper`, and a set of mappers for gatekeeping on security roles and whitelisting dimension filters.

- [Add Table-wide Availability](https://github.com/yahoo/fili/pull/414)
* Add `availableIntervals` field to tables endpoint by union the availability for the logical table without taking
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Add additional points about moving the methods from DataApiRequest to `ApiRequest'

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It would be nice to explain the scope of this PR since it is not the complete feature implementation. Otherwise changes looks good.

Expand Down
379 changes: 379 additions & 0 deletions fili-core/src/main/java/com/yahoo/bard/webservice/web/ApiRequest.java

Large diffs are not rendered by default.

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -3,23 +3,38 @@
package com.yahoo.bard.webservice.web;

import static com.yahoo.bard.webservice.web.ErrorMessageFormat.EMPTY_DICTIONARY;
import static com.yahoo.bard.webservice.web.ErrorMessageFormat.METRICS_UNDEFINED;
import static com.yahoo.bard.webservice.web.ErrorMessageFormat.TABLE_GRANULARITY_MISMATCH;
import static com.yahoo.bard.webservice.web.ErrorMessageFormat.TABLE_UNDEFINED;

import com.yahoo.bard.webservice.data.dimension.Dimension;
import com.yahoo.bard.webservice.data.dimension.DimensionDictionary;
import com.yahoo.bard.webservice.data.metric.LogicalMetric;
import com.yahoo.bard.webservice.data.metric.MetricDictionary;
import com.yahoo.bard.webservice.druid.model.query.Granularity;
import com.yahoo.bard.webservice.logging.RequestLog;
import com.yahoo.bard.webservice.logging.TimedPhase;
import com.yahoo.bard.webservice.table.LogicalTable;
import com.yahoo.bard.webservice.table.LogicalTableDictionary;
import com.yahoo.bard.webservice.table.TableIdentifier;
import com.yahoo.bard.webservice.web.util.BardConfigResources;

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

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.function.Function;
import java.util.stream.Collectors;

import javax.validation.constraints.NotNull;
import javax.ws.rs.core.PathSegment;
import javax.ws.rs.core.UriInfo;

/**
Expand All @@ -32,6 +47,10 @@ public class TablesApiRequest extends ApiRequest {
private final Set<LogicalTable> tables;
private final LogicalTable table;
private final Granularity granularity;
private final Set<Dimension> dimensions;
private final Set<LogicalMetric> metrics;
private final Set<Interval> intervals;
private final Map<Dimension, Set<ApiFilter>> filters;

/**
* Parses the API request URL and generates the Api Request object.
Expand Down Expand Up @@ -76,12 +95,128 @@ public TablesApiRequest(
this.granularity = null;
}

dimensions = Collections.emptySet();
metrics = Collections.emptySet();
intervals = Collections.emptySet();
filters = Collections.emptyMap();

LOG.debug(
"Api request: Tables: {},\nGranularity: {},\nFormat: {}\nPagination: {}",
"Api request: Tables: {},\nGranularity: {},\nFormat: {}\nPagination: {}" +
"\nDimensions: {}\nMetrics: {}\nIntervals: {}\nFilters: {}",
this.tables,
this.granularity,
this.format,
this.paginationParameters
this.paginationParameters,
this.dimensions,
this.metrics,
this.intervals,
this.filters
);
}

/**
* Parses the API request URL and generates the API Request object with specified query constraints, i.e.
* dimensions, metrics, date time intervals, and filters.
*
* @param tableName Logical table corresponding to the table name specified in the URL
* @param granularity Requested time granularity
* @param format Response data format JSON or CSV. Default is JSON.
* @param perPage Number of rows to display per page of results. It must represent a positive integer or an empty
* string if it's not specified
* @param page Desired page of results. It must represent a positive integer or an empty
* string if it's not specified
* @param uriInfo The URI of the request
* @param bardConfigResources The configuration resources used to build this API request
* @param dimensions Grouping dimensions / Dimension constraint
* @param metrics Metrics constraint
* @param intervals Data / Time constraint
* @param filters Filter constraint
* @param timeZoneId A joda time zone id
*
* @throws BadApiRequestException on
* <ol>
* <li>invalid table</li>
* <li>invalid pagination parameters</li>
* </ol>
*/
public TablesApiRequest(
String tableName,
String granularity,
String format,
@NotNull String perPage,
@NotNull String page,
UriInfo uriInfo,
BardConfigResources bardConfigResources,
List<PathSegment> dimensions,
String metrics,
String intervals,
String filters,
String timeZoneId
) throws BadApiRequestException {
super(format, perPage, page, uriInfo);

LogicalTableDictionary logicalTableDictionary = bardConfigResources.getLogicalTableDictionary();
this.tables = generateTables(tableName, logicalTableDictionary);

if (tableName != null && granularity != null) {
this.granularity = generateGranularity(granularity, bardConfigResources.getGranularityParser());
this.table = generateTable(tableName, this.granularity, logicalTableDictionary);
} else {
this.table = null;
this.granularity = null;
}

// parse dimensions
DimensionDictionary dimensionDictionary = bardConfigResources.getDimensionDictionary();
this.dimensions = generateDimensions(dimensions, dimensionDictionary);
validateRequestDimensions(this.dimensions, this.table);

// parse metrics
this.metrics = generateLogicalMetrics(
metrics,
bardConfigResources.getMetricDictionary().getScope(Collections.singletonList(tableName))
);
validateMetrics(this.metrics, this.table);

// parse interval
this.intervals = generateIntervals(
intervals,
this.granularity,
generateDateTimeFormatter(
generateTimeZone(
timeZoneId,
bardConfigResources.getSystemTimeZone()
)
)
);
validateTimeAlignment(this.granularity, this.intervals);

// parse filters
this.filters = generateFilters(filters, table, dimensionDictionary);
validateRequestDimensions(this.filters.keySet(), this.table);

LOG.debug(
"Api request: Tables: {},\nGranularity: {},\nFormat: {}\nPagination: {}" +
"\nDimensions: {}\nMetrics: {}\nIntervals: {}\nFilters: {}",
this.tables,
this.granularity,
this.format,
this.paginationParameters,
this.dimensions.stream()
.map(Dimension::getApiName)
.collect(Collectors.toList()),
this.metrics.stream()
.map(LogicalMetric::getName)
.collect(Collectors.toList()),
this.intervals,
this.filters.entrySet().stream()
.collect(
Collectors.toMap(
entry -> entry.getKey().getApiName(),
Function.identity()

)
)
);
}

Expand All @@ -94,6 +229,10 @@ protected TablesApiRequest() {
this.tables = null;
this.table = null;
this.granularity = null;
this.dimensions = null;
this.metrics = null;
this.intervals = null;
this.filters = null;
}

/**
Expand Down Expand Up @@ -156,6 +295,50 @@ protected LogicalTable generateTable(
return generated;
}

/**
* Extracts the list of metrics from the url metric query string and generates a set of LogicalMetrics.
*
* @param apiMetricQuery URL query string containing the metrics separated by ','.
* @param metricDictionary Metric dictionary contains the map of valid metric names and logical metric objects.
*
* @return Set of metric objects.
* @throws BadApiRequestException if the metric dictionary returns a null or if the apiMetricQuery is invalid.
*/
protected LinkedHashSet<LogicalMetric> generateLogicalMetrics(
String apiMetricQuery,
MetricDictionary metricDictionary
) throws BadApiRequestException {
try (TimedPhase timer = RequestLog.startTiming("GeneratingLogicalMetrics")) {
LOG.trace("Metric dictionary: {}", metricDictionary);

if (apiMetricQuery == null || "".equals(apiMetricQuery)) {
return new LinkedHashSet<>();
}
// set of logical metric objects
LinkedHashSet<LogicalMetric> generated = new LinkedHashSet<>();
List<String> invalidMetricNames = new ArrayList<>();

List<String> metricApiQuery = Arrays.asList(apiMetricQuery.split(","));
for (String metricName : metricApiQuery) {
LogicalMetric logicalMetric = metricDictionary.get(metricName);

// If metric dictionary returns a null, it means the requested metric is not found.
if (logicalMetric == null) {
invalidMetricNames.add(metricName);
} else {
generated.add(logicalMetric);
}
}

if (!invalidMetricNames.isEmpty()) {
LOG.debug(METRICS_UNDEFINED.logFormat(invalidMetricNames.toString()));
throw new BadApiRequestException(METRICS_UNDEFINED.format(invalidMetricNames.toString()));
}
LOG.trace("Generated set of logical metric: {}", generated);
return generated;
}
}

public Set<LogicalTable> getTables() {
return this.tables;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -234,7 +234,7 @@ public Response getTableByGrain(
apiRequest = (TablesApiRequest) requestMapper.apply(apiRequest, containerRequestContext);
}

Map<String, Object> result = getLogicalTableFullView(apiRequest.getTable(), uriInfo);
Map<String, Object> result = getLogicalTableFullView(apiRequest, uriInfo);
String output = objectMappers.getMapper().writeValueAsString(result);
LOG.debug("Tables Endpoint Response: {}", output);
RequestLog.stopTiming(this);
Expand Down Expand Up @@ -368,7 +368,12 @@ public static Map<String, String> getLogicalTableSummaryView(LogicalTable logica
* @param uriInfo UriInfo of the request
*
* @return Full view of the logical table
*
* @deprecated In order to display constrained data availability in table resource, this method needs to accept a
* {@link com.yahoo.bard.webservice.web.TablesApiRequest} as a parameter. Use
* {@link #getLogicalTableFullView(TablesApiRequest, UriInfo)} instead.
*/
@Deprecated
protected static Map<String, Object> getLogicalTableFullView(LogicalTable logicalTable, UriInfo uriInfo) {
Map<String, Object> resultRow = new LinkedHashMap<>();
resultRow.put("category", logicalTable.getCategory());
Expand Down Expand Up @@ -397,6 +402,18 @@ protected static Map<String, Object> getLogicalTableFullView(LogicalTable logica
return resultRow;
}

/**
* Get the full view of the logical table.
*
* @param apiRequest Logical table to get the view of
* @param uriInfo UriInfo of the request
*
* @return Full view of the logical table
*/
protected static Map<String, Object> getLogicalTableFullView(TablesApiRequest apiRequest, UriInfo uriInfo) {
return getLogicalTableFullView(apiRequest.getTable(), uriInfo);
}

/**
* Get the URL of the logical table.
*
Expand Down
Loading