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

#30192 adding the date range parsing #30208

Merged
merged 14 commits into from
Oct 8, 2024
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
Original file line number Diff line number Diff line change
Expand Up @@ -129,11 +129,8 @@ public CubeJSQuery parseQueryToCubeQuery(final AnalyticsQuery query) {
}

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());
final CubeJSQuery.TimeDimension parsedTimeDimension = TimeDimensionParser.parseTimeDimension(timeDimensions);
return Stream.of(parsedTimeDimension).collect(Collectors.toList());
}

private Collection<CubeJSQuery.OrderItem> parseOrders(final String orders) {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
package com.dotcms.analytics.query;

import com.dotcms.cube.CubeJSQuery;

import java.util.regex.Matcher;
import java.util.regex.Pattern;

Expand All @@ -20,42 +22,20 @@ private TimeDimensionParser() {
// singleton
}

private static final String FIELD_REGEX = "(\\w+\\.\\w+)\\s+(\\w+)";

public static class TimeDimension {
private String term;
private String field;

public TimeDimension(final String term, final String field) {
this.term = term;
this.field = field;
}

public String getTerm() {
return term;
}

public String getField() {
return field;
}

@Override
public String toString() {
return "Term: " + term + ", Field: " + field;
}
}
private static final String FIELD_REGEX = "^(\\w+\\.\\w+)\\s+(\\w+)(?:\\s+(.+))?$";
private static final Pattern PATTERN = Pattern.compile(FIELD_REGEX);

public static TimeDimension parseTimeDimension(final String expression) throws IllegalArgumentException {
public static CubeJSQuery.TimeDimension parseTimeDimension(final String expression) throws IllegalArgumentException {
// cache and checked
final Pattern pattern = Pattern.compile(FIELD_REGEX);
final Matcher matcher = pattern.matcher(expression.trim());
final Matcher matcher = PATTERN.matcher(expression.trim());

if (matcher.matches()) {

final String term = matcher.group(1); // Ex: Events.day
final String field = matcher.group(2); // Ex: day
final String dimension = matcher.group(1); // Ex: Events.day
final String granularity = matcher.group(2); // Ex: day
final String dateRange = matcher.group(3); // Ex: date range

return new TimeDimension(term, field);
return new CubeJSQuery.TimeDimension(dimension, granularity, dateRange);
} else {
throw new IllegalArgumentException("The expression is not valid. This should be the format 'Term Field'.");
}
Expand Down
178 changes: 104 additions & 74 deletions dotCMS/src/main/java/com/dotcms/cube/CubeJSQuery.java
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
package com.dotcms.cube;


import com.dotcms.cube.filters.Filter;
import com.dotcms.cube.filters.Filter.Order;
import com.dotcms.cube.filters.LogicalFilter;
Expand All @@ -22,83 +21,88 @@
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.stream.Collectors;


/**
* Represents a Cube JS Query
* You can use the {@link Builder} to create a CubeJSQuery and later using the
* {@link CubeJSQuery#toString()}.
*
* Examples:
*
* <code>
* Represents a CubeJS Query. You can use the {@link Builder} to create a CubeJSQuery, and then,
* call the {@link CubeJSQuery#toString()} method to generate it. For instance, you can have the
* following Java code:
* <pre>
* {@code
* final CubeJSQuery cubeJSQuery = new Builder()
* .dimensions("Events.experiment")
* .measures("Events.count")
* .filter("Events.variant", SimpleFilter.Operator.EQUALS, "B")
* .build();
* </code>
*
* To get:
*
* <code>
* }
* </pre>
* To generate this CubeJS query:
* <pre>
* {@code
* {
* "dimensions": [
* "Events.experiment"
* ],
* {
* "measures": [
* "Events.count"
* ],
* filters: [
* {
* member: "Events.variant",
* operator: "equals",
* values: ["B"]
* }
* ]
* "dimensions": [
* "Events.experiment"
* ],
* {
* "measures": [
* "Events.count"
* ],
* filters: [
* {
* member: "Events.variant",
* operator: "equals",
* values: ["B"]
* }
* ]
* }
* }
* }
* </code>
*
* @see <a href="https://cube.dev/docs/query-format">CubeJS Query format</a>
* </pre>
* <p>
* For more information on the CbeJS query format, please refer to the <a
* href="https://cube.dev/docs/query-format">official CubeJS Query format documentation.</a>
*/
public class CubeJSQuery {

private String[] dimensions;
private String[] measures;
private Filter[] filters;

private OrderItem[] orders;
private final String[] dimensions;
private final String[] measures;
private final Filter[] filters;

private long limit = -1;
private long offset = -1;
private TimeDimension[] timeDimensions;
private final OrderItem[] orders;
private final TimeDimension[] timeDimensions;

private final long limit;
private final long offset;

private CubeJSQuery(final Builder builder) {
this.dimensions = builder.dimensions;
this.measures = builder.measures;
this.filters = builder.filters.toArray(new Filter[builder.filters.size()]);
this.orders = builder.orders.toArray(new OrderItem[builder.orders.size()]);
this.filters = builder.filters.toArray(new Filter[0]);
this.orders = builder.orders.toArray(new OrderItem[0]);
this.limit = builder.limit;
this.offset = builder.offset;
this.timeDimensions = builder.timeDimensions.toArray(new TimeDimension[builder.timeDimensions.size()]);
this.timeDimensions = builder.timeDimensions.toArray(new TimeDimension[0]);
}

@Override
public String toString() {
if (!UtilMethods.isSet(dimensions) && !UtilMethods.isSet(measures)) {
throw new IllegalStateException("Must set dimensions or measures");
throw new IllegalStateException("The 'dimensions' and 'measures' parameters must be set");
}

try {
return JsonUtil.getJsonAsString(getMap());
} catch (IOException e) {
} catch (final IOException e) {
throw new RuntimeException(e);
}
}

/**
* Returns the CubeJS Query as a Map composed of its different parameters. Keep in mind that
* this is used for both generating the actual CubeJS query, and the JSON data as String.
*
* @return The CubeJS Query as a Map of attributes and their values.
*/
private Map<String, Object> getMap() {
final Map<String, Object> map = new HashMap<>();

Expand Down Expand Up @@ -127,7 +131,19 @@ private Map<String, Object> getMap() {
}

if (timeDimensions.length > 0) {
map.put("timeDimensions", timeDimensions);
final Set<Map<String, Object>> correctedTimeDimensions = Arrays.stream(this.timeDimensions)
.map(timeDimension -> {
final Map<String, Object> dataMap = new LinkedHashMap<>();
dataMap.put("dimension", timeDimension.getDimension());
dataMap.put("granularity", timeDimension.getGranularity());
if (UtilMethods.isSet(timeDimension.getDateRange())) {
// If the 'dateRange' parameter is not set, then it must NOT be
// part of the data map, or the CubeJS query will fail
dataMap.put("dateRange", timeDimension.getDateRange());
}
return dataMap;
}).collect(Collectors.toSet());
map.put("timeDimensions", correctedTimeDimensions);
}

return map;
Expand All @@ -145,7 +161,7 @@ private Map<String, String> getOrdersAsMap() {
}
private List<Map<String, Object>> getFiltersAsMap() {
return Arrays.stream(filters)
.map(filter -> filter.asMap())
.map(Filter::asMap)
.collect(Collectors.toList());
}

Expand Down Expand Up @@ -203,50 +219,51 @@ public static class Builder {
private Collection<OrderItem> orders = new ArrayList<>();
private long limit = -1;
private long offset = -1;
private List<TimeDimension> timeDimensions = new ArrayList<>();
private final List<TimeDimension> timeDimensions = new ArrayList<>();

/**
* Merge two {@link CubeJSQuery}, each section of the Query is merge ignoring duplicated values
* for example If we have:
*
* Query 1:
* <code>
* Merges two {@link CubeJSQuery} objects. Each section of the Query is merged by ignoring
* duplicate values. For instance, if we have the following queries:
* <pre>
* Query #1:
* {@code
* {
* "dimensions": [
* "Events.Experiment",
* "Events.variant"
* ]
* }
* </code>
*
* * Query 2:
* Query 1:
* <code>
* }
* </pre>
* <pre>
* Query #2:
* {@code
* {
* "dimensions": [
* "Events.Experiment",
* "Events.eventType"
* ]
* }
* </code>
*
* }
* </pre>
* The result is going to be:
*
* <code>
* <pre>
* {@code
* {
* "dimensions": [
* "Events.Experiment",
* "Events.variant",
* "Events.eventType"
* ]
* }
* </code>
* }
* </pre>
* The same will happen with others section such as: measures, filters and order.
*
* The same happens with others section like: measures, filters and order.
* @param cubeJSQuery1 The first {@link CubeJSQuery} to merge.
* @param cubeJSQuery2 The second {@link CubeJSQuery} to merge.
*
* @param cubeJSQuery1
* @param cubeJSQuery2
* @return
* @return A new {@link CubeJSQuery} object with the merged values.
*/
public static CubeJSQuery merge(final CubeJSQuery cubeJSQuery1, final CubeJSQuery cubeJSQuery2) {
final Collection<String> dimensionsMerged = merge(
Expand Down Expand Up @@ -289,12 +306,12 @@ private static <T> List<T> getEmptyIfIsNotSet(T[] array) {
}

public Builder dimensions(final Collection<String> dimensions) {
this.dimensions = dimensions.toArray(new String[dimensions.size()]);
this.dimensions = dimensions.toArray(new String[0]);
return this;
}

public Builder measures(final Collection<String> measures) {
this.measures = measures.toArray(new String[measures.size()]);
this.measures = measures.toArray(new String[0]);
return this;
}

Expand Down Expand Up @@ -354,7 +371,7 @@ public Builder order(final String orderBy, final Order order) {

public Builder limit(final long limit) {

DotPreconditions.checkArgument(limit >= 0, "Limit must be greater than 0");
DotPreconditions.checkArgument(limit >= 0, "Limit must be greater than or equal to 0");
DotPreconditions.checkArgument(limit <= 50000, "Limit must be less than or equal to 50000");

this.limit = limit;
Expand All @@ -368,7 +385,11 @@ public Builder offset(final long offset) {
}

public Builder timeDimension(final String dimension, final String granularity) {
this.timeDimensions.add(new TimeDimension(dimension, granularity));
return timeDimension(dimension, granularity, null);
}

public Builder timeDimension(final String dimension, final String granularity, final String dateRange) {
this.timeDimensions.add(new TimeDimension(dimension, granularity, dateRange));
return this;
}

Expand All @@ -381,10 +402,14 @@ public Builder timeDimensions(Collection<TimeDimension> timeDimensions) {
public static class TimeDimension {
String dimension;
String granularity;
String dateRange;

public TimeDimension(String dimension, String granularity) {
public TimeDimension(final String dimension,
final String granularity,
final String dateRange) {
this.dimension = dimension;
this.granularity = granularity;
this.dateRange = dateRange;
}

public String getDimension() {
Expand All @@ -394,11 +419,15 @@ public String getDimension() {
public String getGranularity() {
return granularity;
}

public String getDateRange() {
return dateRange;
}
}

public static class OrderItem {
private String orderBy;
private Order order;
private final String orderBy;
private final Order order;

public OrderItem(final String orderBy, final Order order) {
this.orderBy = orderBy;
Expand Down Expand Up @@ -430,4 +459,5 @@ public int hashCode() {
return Objects.hash(orderBy, order);
}
}

}
Loading