Skip to content

Commit

Permalink
Issue 29865 query parser (#30004)
Browse files Browse the repository at this point in the history
Draft of the query parser for analytics

---------

Co-authored-by: freddyDOTCMS <[email protected]>
Co-authored-by: Jose Castro <[email protected]>
Co-authored-by: freddyDOTCMS <[email protected]>
  • Loading branch information
4 people authored Sep 19, 2024
1 parent 358f66d commit 702df61
Show file tree
Hide file tree
Showing 12 changed files with 984 additions and 10 deletions.
2 changes: 0 additions & 2 deletions dotCMS/src/main/java/com/dotcms/analytics/Util.java
Original file line number Diff line number Diff line change
@@ -1,10 +1,8 @@
package com.dotcms.analytics;

import com.dotmarketing.beans.Host;
import com.dotmarketing.business.APILocator;
import com.dotmarketing.cms.urlmap.UrlMapContext;
import com.dotmarketing.util.Logger;
import com.dotmarketing.util.PageMode;
import io.vavr.control.Try;

import static com.dotcms.exception.ExceptionUtil.getErrorMessage;
Expand Down
158 changes: 158 additions & 0 deletions dotCMS/src/main/java/com/dotcms/analytics/query/AnalyticsQuery.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
package com.dotcms.analytics.query;

import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;

import java.util.Set;

/**
* Encapsulates a simple query for the analytics backend
* Example:
* <pre>
* {
* "query": {
* "dimensions": [
* "Events.experiment",
* "Events.variant",
* "Events.lookBackWindow"
* ],
* "measures": [
* "Events.count"
* ],
* "filters": "Events.variant = ['B']",
* "limit": 100,
* "offset": 1,
* "timeDimensions": "Events.day day",
* "orders": "Events.day ASC"
* }
* }
*
* @see AnalyticsQueryParser
* </pre>
* @author jsanca
*/
@JsonDeserialize(builder = AnalyticsQuery.Builder.class)
public class AnalyticsQuery {

private final Set<String> dimensions; // ["Events.referer", "Events.experiment", "Events.variant", "Events.utcTime", "Events.url", "Events.lookBackWindow", "Events.eventType"]
private final Set<String> measures; // ["Events.count", "Events.uniqueCount"]
private final String filters; // Events.variant = ["B"] or Events.experiments = ["B"]
private final long limit;
private final long offset;
private final String timeDimensions; // Events.day day
private String orders; // Events.day ASC

private AnalyticsQuery(final Builder builder) {
this.dimensions = builder.dimensions;
this.measures = builder.measures;
this.filters = builder.filters;
this.limit = builder.limit;
this.offset = builder.offset;
this.timeDimensions = builder.timeDimensions;
this.orders = builder.orders;
}

public Set<String> getDimensions() {
return dimensions;
}

public Set<String> getMeasures() {
return measures;
}

public String getFilters() {
return filters;
}

public long getLimit() {
return limit;
}

public long getOffset() {
return offset;
}

public String getTimeDimensions() {
return timeDimensions;
}

public String getOrders() {
return orders;
}

public static class Builder {

@JsonProperty()
private Set<String> dimensions;
@JsonProperty()
private Set<String> measures;
@JsonProperty()
private String filters;
@JsonProperty()
private long limit;
@JsonProperty()
private long offset;
@JsonProperty()
private String timeDimensions;
@JsonProperty()
private String orders;


public Builder dimensions(Set<String> dimensions) {
this.dimensions = dimensions;
return this;
}

public Builder measures(Set<String> measures) {
this.measures = measures;
return this;
}

public Builder filters(String filters) {
this.filters = filters;
return this;
}

public Builder limit(long limit) {
this.limit = limit;
return this;
}

public Builder offset(long offset) {
this.offset = offset;
return this;
}

public Builder timeDimensions(String timeDimensions) {
this.timeDimensions = timeDimensions;
return this;
}

public Builder orders(String orders) {
this.orders = orders;
return this;
}

public AnalyticsQuery build() {
return new AnalyticsQuery(this);
}
}

public static Builder builder() {
return new Builder();
}

@Override
public String toString() {
return "AnalyticsQuery{" +
"dimensions=" + dimensions +
", measures=" + measures +
", filters='" + filters + '\'' +
", limit=" + limit +
", offset=" + offset +
", timeDimensions='" + timeDimensions + '\'' +
", orders='" + orders + '\'' +
'}';
}
}

Original file line number Diff line number Diff line change
@@ -0,0 +1,192 @@
package com.dotcms.analytics.query;

import com.dotcms.cube.CubeJSQuery;
import com.dotcms.cube.filters.Filter;
import com.dotcms.cube.filters.LogicalFilter;
import com.dotcms.cube.filters.SimpleFilter;
import com.dotcms.rest.api.v1.DotObjectMapperProvider;
import com.dotmarketing.exception.DotRuntimeException;
import com.dotmarketing.util.Logger;
import com.dotmarketing.util.UtilMethods;
import com.fasterxml.jackson.core.JsonProcessingException;
import io.vavr.Tuple2;

import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Objects;
import java.util.stream.Collectors;
import java.util.stream.Stream;

/**
* Parser for the analytics query, it can parse a json string to a {@link AnalyticsQuery} or a {@link CubeJSQuery}
* @author jsanca
*/
public class AnalyticsQueryParser {

/**
* Parse a json string to a {@link AnalyticsQuery}
* Example:
* {
* "dimensions": ["Events.referer", "Events.experiment", "Events.variant", "Events.utcTime", "Events.url", "Events.lookBackWindow", "Events.eventType"],
* "measures": ["Events.count", "Events.uniqueCount"],
* "filters": "Events.variant = ['B'] or Events.experiments = ['B']",
* "limit":100,
* "offset":1,
* "timeDimensions":"Events.day day",
* "orders":"Events.day ASC"
* }
* @param json
* @return AnalyticsQuery
*/
public AnalyticsQuery parseJsonToQuery(final String json) {

if (Objects.isNull(json)) {
throw new IllegalArgumentException("Json can not be null");
}
try {

Logger.debug(this, ()-> "Parsing json to query: " + json);
return DotObjectMapperProvider.getInstance().getDefaultObjectMapper()
.readValue(json, AnalyticsQuery.class);
} catch (JsonProcessingException e) {
Logger.error(this, e.getMessage(), e);
throw new DotRuntimeException(e);
}
}

/**
* Parse a json string to a {@link CubeJSQuery}
* Example:
* {
* "dimensions": ["Events.referer", "Events.experiment", "Events.variant", "Events.utcTime", "Events.url", "Events.lookBackWindow", "Events.eventType"],
* "measures": ["Events.count", "Events.uniqueCount"],
* "filters": "Events.variant = ['B'] or Events.experiments = ['B']",
* "limit":100,
* "offset":1,
* "timeDimensions":"Events.day day",
* "orders":"Events.day ASC"
* }
* @param json
* @return CubeJSQuery
*/
public CubeJSQuery parseJsonToCubeQuery(final String json) {

Logger.debug(this, ()-> "Parsing json to cube query: " + json);
final AnalyticsQuery query = parseJsonToQuery(json);
return parseQueryToCubeQuery(query);
}

/**
* Parse an {@link AnalyticsQuery} to a {@link CubeJSQuery}
* @param query
* @return CubeJSQuery
*/
public CubeJSQuery parseQueryToCubeQuery(final AnalyticsQuery query) {

if (Objects.isNull(query)) {
throw new IllegalArgumentException("Query can not be null");
}

final CubeJSQuery.Builder builder = new CubeJSQuery.Builder();
Logger.debug(this, ()-> "Parsing query to cube query: " + query);

if (UtilMethods.isSet(query.getDimensions())) {
builder.dimensions(query.getDimensions());
}

if (UtilMethods.isSet(query.getMeasures())) {
builder.measures(query.getMeasures());
}

if (UtilMethods.isSet(query.getFilters())) {
builder.filters(parseFilters(query.getFilters()));
}

builder.limit(query.getLimit()).offset(query.getOffset());

if (UtilMethods.isSet(query.getOrders())) {
builder.orders(parseOrders(query.getOrders()));
}

if (UtilMethods.isSet(query.getTimeDimensions())) {
builder.timeDimensions(parseTimeDimensions(query.getTimeDimensions()));
}

return builder.build();
}

private Collection<CubeJSQuery.TimeDimension> parseTimeDimensions(final String timeDimensions) {
final TimeDimensionParser.TimeDimension parsedTimeDimension = TimeDimensionParser.parseTimeDimension(timeDimensions);
return Stream.of(
new CubeJSQuery.TimeDimension(parsedTimeDimension.getTerm(),
parsedTimeDimension.getField())
).collect(Collectors.toList());
}

private Collection<CubeJSQuery.OrderItem> parseOrders(final String orders) {

final OrderParser.ParsedOrder parsedOrder = OrderParser.parseOrder(orders);
return Stream.of(
new CubeJSQuery.OrderItem(parsedOrder.getTerm(),
"ASC".equalsIgnoreCase(parsedOrder.getOrder())?
Filter.Order.ASC:Filter.Order.DESC)
).collect(Collectors.toList());
}

private Collection<Filter> parseFilters(final String filters) {
final Tuple2<List<FilterParser.Token>,List<FilterParser.LogicalOperator>> result =
FilterParser.parseFilterExpression(filters);

final List<Filter> filterList = new ArrayList<>();
final List<SimpleFilter> simpleFilters = new ArrayList<>();

for (final FilterParser.Token token : result._1) {

simpleFilters.add(
new SimpleFilter(token.member,
parseOperator(token.operator),
new Object[]{token.values}));
}

// if has operators
if (UtilMethods.isSet(result._2())) {

FilterParser.LogicalOperator logicalOperator = result._2().get(0); // first one
LogicalFilter.Builder logicalFilterBuilder = logicalOperator == FilterParser.LogicalOperator.AND?
LogicalFilter.Builder.and():LogicalFilter.Builder.or();

LogicalFilter logicalFilterFirst = logicalFilterBuilder.add(simpleFilters.get(0)).add(simpleFilters.get(1)).build();
for (int i = 1; i < result._2().size(); i++) { // nest the next ones

logicalOperator = result._2().get(i);
logicalFilterBuilder = logicalOperator == FilterParser.LogicalOperator.AND?
LogicalFilter.Builder.and():LogicalFilter.Builder.or();

logicalFilterFirst = logicalFilterBuilder.add(logicalFilterFirst)
.add(simpleFilters.get(i + 1)).build();
}

filterList.add(logicalFilterFirst);
} else {
filterList.addAll(simpleFilters);
}

return filterList;
}

private SimpleFilter.Operator parseOperator(final String operator) {
switch (operator) {
case "=":
return SimpleFilter.Operator.EQUALS;
case "!=":
return SimpleFilter.Operator.NOT_EQUALS;
case "in":
return SimpleFilter.Operator.CONTAINS;
case "!in":
return SimpleFilter.Operator.NOT_CONTAINS;
default:
throw new DotRuntimeException("Operator not supported: " + operator);
}
}
}
Loading

0 comments on commit 702df61

Please sign in to comment.