Skip to content

Commit

Permalink
feat(content analytics) fixes #30521 : Allow users to pass down simpl…
Browse files Browse the repository at this point in the history
…e Strings to query for Content Analytics data (#30869)

### Proposed Changes
* After discussion with the team, here's the new data format for the
simple String query. By default, the `request` scheme is prepended to
the appropriate terms so users won't have to repeat so many times:
```json
{
    "measures": "count,totalSessions",
    "dimensions": "host,whatAmI,url",
    "timeDimensions": "createdAt,day:Last month",
    "filters": "totalRequest gt 0,whatAmI contains PAGE||FILE",
    "order": "count asc,createdAt asc",
    "limit": 50,
    "offset": 0
}
```
If you want to set a specific scheme, just pass it down in the JSON body
like this:
```json
{
    "scheme": "YOUR-SCHEME-NAME-HERE",
     ...
     ...
}
```
The following must be taken into account when putting a query together:
  * Measures: Values are separated by commas: `count,totalSessions`
  * Dimensions: Values are separated by commas: `host,whatAmI,url`
* Time Dimensions: Values are separated by a comma: `createdAt,day:Last
month` . The second parameter 'day' -- the "granularity" parameter -- is
optional, and separated from the date range by a colon.
* Filters: Values are separated by commas: `totalRequest gt 0,whatAmI
contains PAGE||FILE` . In the `contains` clause, values are separated by
`||`.
  * Order: Values are separated by comma: `count asc,createdAt asc`
  * Limit: Value is provided as is: `50`
  * Offset: Value is provided as is: `0`

This PR fixes: #30521
  • Loading branch information
jcastro-dotcms authored Dec 6, 2024
1 parent d19b399 commit 363774c
Show file tree
Hide file tree
Showing 4 changed files with 296 additions and 47 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -10,21 +10,57 @@
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;

import static com.liferay.util.StringPool.COLON;
import static com.liferay.util.StringPool.COMMA;
import static com.liferay.util.StringPool.PERIOD;

/**
* This class represents the parameters of a Content Analytics Query abstracting the complexity
* of the underlying JSON format. The simplified REST Endpoint and the Content Analytics ViewTool
* use this class so that parameters can be entered in a more user-friendly way.
* use this class so that parameters can be entered in a more user-friendly way. Here's an example
* of what this simple JSON data looks like for the default {@code request} schema:
* <pre>
* {@code
* {
* "measures": "count,totalSessions",
* "dimensions": "host,whatAmI,url",
* "timeDimensions": "createdAt,day:Last month",
* "filters": "totalRequest gt 0,whatAmI contains PAGE||FILE",
* "order": "count asc,createdAt asc",
* "limit": 5,
* "offset": 0
* }
* }
* </pre>
* Under the covers, this builder will prefix the appropriate terms with the specified or default
* schema. If you want to provide a specific one, just add it to the JSON body:
* <pre>
* {@code
* {
* "scheme": "YOUR-SCHEME-NAME-HERE",
* ...
* ...
* }
* }
* </pre>
* Notice how there are four separator characters for different parameters. They must be used
* correctly for the data to be parsed correctly:
* <ul>
* <li>Blank space.</li>
* <li>Comma.</li>
* <li>Colon.</li>
* <li>Double pipes.</li>
* </ul>
*
* @author Jose Castro
* @since Nov 28th, 2024
*/
@JsonDeserialize(builder = ContentAnalyticsQuery.Builder.class)
public class ContentAnalyticsQuery implements Serializable {

public static final String SCHEME_ATTR = "scheme";
public static final String MEASURES_ATTR = "measures";
public static final String DIMENSIONS_ATTR = "dimensions";
public static final String TIME_DIMENSIONS_ATTR = "timeDimensions";
Expand All @@ -39,6 +75,7 @@ public class ContentAnalyticsQuery implements Serializable {
public static final String OPERATOR_ATTR = "operator";
public static final String VALUES_ATTR = "values";

private final String scheme;
@JsonProperty()
private final Set<String> measures;
@JsonProperty()
Expand All @@ -54,9 +91,13 @@ public class ContentAnalyticsQuery implements Serializable {
@JsonProperty()
private final int offset;

private static final String SEPARATOR = COLON;
private static final String SPACE = "\\s+";
private static final String DOUBLE_PIPE = "\\|\\|";
private static final String DEFAULT_DATE_RANGE = "Last week";
private static final String DEFAULT_SCHEME = "request";

private ContentAnalyticsQuery(final Builder builder) {
this.scheme = builder.scheme;
this.measures = builder.measures;
this.dimensions = builder.dimensions;
this.timeDimensions = builder.timeDimensions;
Expand All @@ -66,6 +107,10 @@ private ContentAnalyticsQuery(final Builder builder) {
this.offset = builder.offset;
}

public String scheme() {
return this.scheme;
}

public Set<String> measures() {
return this.measures;
}
Expand Down Expand Up @@ -101,13 +146,14 @@ public static ContentAnalyticsQuery.Builder builder() {
@Override
public String toString() {
return "ContentAnalyticsQuery{" +
"measures='" + measures + '\'' +
", dimensions='" + dimensions + '\'' +
", timeDimensions='" + timeDimensions + '\'' +
", filters='" + filters + '\'' +
", order='" + order + '\'' +
", limit='" + limit + '\'' +
", offset='" + offset + '\'' +
"scheme='" + scheme + '\'' +
", measures=" + measures +
", dimensions=" + dimensions +
", timeDimensions=" + timeDimensions +
", filters=" + filters +
", order=" + order +
", limit=" + limit +
", offset=" + offset +
'}';
}

Expand All @@ -117,6 +163,7 @@ public String toString() {
*/
public static class Builder {

private String scheme = DEFAULT_SCHEME;
private Set<String> measures;
private Set<String> dimensions;
private final List<Map<String, String>> timeDimensions = new ArrayList<>();
Expand All @@ -125,16 +172,29 @@ public static class Builder {
private int limit = 1000;
private int offset = 0;

/**
* Sets the default scheme for the parameters sent to the Content Analytics service.
*
* @param scheme The default scheme for the parameters.
*
* @return The builder instance.
*/
public Builder scheme(final String scheme) {
this.scheme = scheme;
return this;
}

/**
* The measures parameter contains a set of measures and each measure is an aggregation over
* a certain column in your ClickHouse database table.
*
* @param measures A string with the measures separated by a space.
* @param measures A string with the measures separated by
* {@link com.liferay.util.StringPool#COMMA}.
*
* @return The builder instance.
*/
public Builder measures(final String measures) {
this.measures = Set.of(measures.split("\\s+"));
this.measures = addScheme(Set.of(measures.split(COMMA)));
return this;
}

Expand All @@ -143,12 +203,13 @@ public Builder measures(final String measures) {
* an attribute related to a measure, e.g. the measure user_count can have dimensions like
* country, age, occupation, etc.
*
* @param dimensions A string with the dimensions separated by a space.
* @param dimensions A string with the dimensions separated by
* {@link com.liferay.util.StringPool#COMMA}.
*
* @return The builder instance.
*/
public Builder dimensions(final String dimensions) {
this.dimensions = Set.of(dimensions.split("\\s+"));
this.dimensions = addScheme(Set.of(dimensions.split(COMMA)));
return this;
}

Expand All @@ -157,24 +218,28 @@ public Builder dimensions(final String dimensions) {
* an array of objects in timeDimension format. If no date range is provided, the default
* value will be "Last week".
*
* @param timeDimensions A string with the time dimensions separated by a colon.
* @param timeDimensions A string with the time dimensions separated by
* {@link com.liferay.util.StringPool#COMMA}.
*
* @return The builder instance.
*/
public Builder timeDimensions(final String timeDimensions) {
if (UtilMethods.isNotSet(timeDimensions)) {
return this;
}
final String[] timeParams = timeDimensions.split(SEPARATOR);
final String[] timeParams = timeDimensions.split(COMMA);
final Map<String, String> timeDimensionsData = new HashMap<>();
timeDimensionsData.put(TIME_DIMENSIONS_DIMENSION_ATTR, timeParams[0]);
if (timeParams.length > 2) {
timeDimensionsData.put(GRANULARITY_ATTR, timeParams[1]);
timeDimensionsData.put(DATE_RANGE_ATTR, timeParams[2]);
} else if (timeParams.length > 1) {
timeDimensionsData.put(DATE_RANGE_ATTR, timeParams[1]);
timeDimensionsData.put(TIME_DIMENSIONS_DIMENSION_ATTR, addScheme(timeParams[0]));
if (timeParams.length > 1) {
final String[] granularityAndRange = timeParams[1].split(COLON);
if (granularityAndRange.length > 1) {
timeDimensionsData.put(GRANULARITY_ATTR, granularityAndRange[0]);
timeDimensionsData.put(DATE_RANGE_ATTR, granularityAndRange[1]);
} else {
timeDimensionsData.put(DATE_RANGE_ATTR, granularityAndRange[0]);
}
} else {
timeDimensionsData.put(DATE_RANGE_ATTR, "Last week");
timeDimensionsData.put(DATE_RANGE_ATTR, DEFAULT_DATE_RANGE);
}
this.timeDimensions.add(timeDimensionsData);
return this;
Expand All @@ -184,23 +249,24 @@ public Builder timeDimensions(final String timeDimensions) {
* Filters are applied differently to dimensions and measures. When you filter on a
* dimension, you are restricting the raw data before any calculations are made. When you
* filter on a measure, you are restricting the results after the measure has been
* calculated. They are composed of: member, operator, and values.
* calculated. They are composed of 3 parts: member, operator, and values.
*
* @param filters A string with the filters separated by a colon.
* @param filters A string with the filters separated by
* {@link com.liferay.util.StringPool#COMMA}.
*
* @return The builder instance.
*/
public Builder filters(final String filters) {
if (UtilMethods.isNotSet(filters)) {
return this;
}
final String[] filterArr = filters.split(SEPARATOR);
final String[] filterArr = filters.split(COMMA);
for (final String filter : filterArr) {
final String[] filterParams = filter.split("\\s+");
final String[] filterParams = filter.split(SPACE);
final Map<String, Object> filterDataMap = new HashMap<>();
filterDataMap.put(MEMBER_ATTR, filterParams[0]);
filterDataMap.put(MEMBER_ATTR, addScheme(filterParams[0]));
filterDataMap.put(OPERATOR_ATTR, filterParams[1]);
final String[] filterValues = filterParams[2].split(COMMA);
final String[] filterValues = filterParams[2].split(DOUBLE_PIPE);
filterDataMap.put(VALUES_ATTR, filterValues);
this.filters.add(filterDataMap);
}
Expand All @@ -213,18 +279,23 @@ public Builder filters(final String filters) {
* on the order of the keys in the object. If not provided, default ordering is applied. If
* an empty object ([]) is provided, no ordering is applied.
*
* @param order A string with the order separated by a colon.
* @param order A string with the order separated by
* {@link com.liferay.util.StringPool#COMMA}.
*
* @return The builder instance.
*/
public Builder order(final String order) {
if (UtilMethods.isNotSet(order)) {
return this;
}
final Set<String> orderCriteria = Set.of(order.split(SEPARATOR));
final Set<String> orderCriteria = Set.of(order.split(COMMA));
for (final String orderCriterion : orderCriteria) {
final String[] orderParams = orderCriterion.split("\\s+");
this.order.add(orderParams);
final String[] orderParams = orderCriterion.split(SPACE);
if (orderParams.length > 1) {
this.order.add(new String[]{addScheme(orderParams[0]), orderParams[1]});
} else {
this.order.add(orderParams);
}
}
return this;
}
Expand Down Expand Up @@ -253,7 +324,6 @@ public Builder offset(final int offset) {
return this;
}


/**
* This method builds the ContentAnalyticsQuery object based on all the specified
* parameters for the query.
Expand All @@ -268,21 +338,46 @@ public ContentAnalyticsQuery build() {
* This method builds the ContentAnalyticsQuery object based on all the specified
* parameters in the provided map.
*
* @param form A {@link Map} containing the query data.
* @param parameters A {@link Map} containing the query data.
*
* @return The {@link ContentAnalyticsQuery} object.
*/
public ContentAnalyticsQuery build(final Map<String, Object> form) {
this.measures((String) form.get(MEASURES_ATTR));
this.dimensions((String) form.get(DIMENSIONS_ATTR));
this.timeDimensions((String) form.get(TIME_DIMENSIONS_ATTR));
this.filters((String) form.get(FILTERS_ATTR));
this.order((String) form.get(ORDER_ATTR));
this.limit((Integer) form.get(LIMIT_ATTR));
this.offset((Integer) form.get(OFFSET_ATTR));
public ContentAnalyticsQuery build(final Map<String, Object> parameters) {
this.scheme((String) parameters.getOrDefault(SCHEME_ATTR, DEFAULT_SCHEME));
this.measures((String) parameters.get(MEASURES_ATTR));
this.dimensions((String) parameters.get(DIMENSIONS_ATTR));
this.timeDimensions((String) parameters.get(TIME_DIMENSIONS_ATTR));
this.filters((String) parameters.get(FILTERS_ATTR));
this.order((String) parameters.get(ORDER_ATTR));
this.limit((Integer) parameters.get(LIMIT_ATTR));
this.offset((Integer) parameters.get(OFFSET_ATTR));
return new ContentAnalyticsQuery(this);
}

/**
* This method adds the default scheme to the terms if they don't contain it.
*
* @param terms The terms to check.
*
* @return The terms with the default scheme added if they don't contain it.
*/
private Set<String> addScheme(final Set<String> terms) {
return terms.stream()
.map(this::addScheme)
.collect(Collectors.toSet());
}

/**
* This method adds the default scheme to the term if it doesn't contain it.
*
* @param term The term to check.
*
* @return The term with the default scheme added if it doesn't contain it.
*/
private String addScheme(final String term) {
return term.contains(PERIOD) ? term : scheme + PERIOD + term;
}

}

}
Loading

0 comments on commit 363774c

Please sign in to comment.