Skip to content

Commit

Permalink
Merge branch 'main' into issue-30192-time-range
Browse files Browse the repository at this point in the history
  • Loading branch information
jdotcms authored Oct 2, 2024
2 parents b6fda59 + 9252e3e commit 97cf17c
Show file tree
Hide file tree
Showing 22 changed files with 540 additions and 101 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -31,4 +31,11 @@ public interface ContentAnalyticsAPI {
*/
ReportResponse runRawReport(CubeJSQuery cubeJSQuery, User user);

/**
* Runs a raw report based on a cubeJS json string query
* @param cubeJsQueryJson
* @param user
* @return ReportResponse
*/
ReportResponse runRawReport(String cubeJsQueryJson, User user);
}
Original file line number Diff line number Diff line change
Expand Up @@ -26,17 +26,23 @@ public ContentAnalyticsAPIImpl(final ContentAnalyticsFactory contentAnalyticsFac

@Override
public ReportResponse runReport(final AnalyticsQuery query, final User user) {

Logger.debug(this, ()-> "Running the report for the query: " + query);
// note: should check any permissions for an user.
// TODO: We should check for specific user permissions
return this.contentAnalyticsFactory.getReport(query, user);
}

@Override
public ReportResponse runRawReport(CubeJSQuery cubeJSQuery, User user) {
public ReportResponse runRawReport(final CubeJSQuery cubeJSQuery, final User user) {
Logger.debug(this, ()-> "Running the report for the raw query: " + cubeJSQuery);
// note: should check any permissions for an user.
// TODO: We should check for specific user permissions
return this.contentAnalyticsFactory.getRawReport(cubeJSQuery, user);
}

@Override
public ReportResponse runRawReport(final String cubeJsQueryJson, final User user) {

Logger.debug(this, ()-> "Running the report for the raw json query: " + cubeJsQueryJson);
// note: should check any permissions for an user.
return this.contentAnalyticsFactory.getRawReport(cubeJsQueryJson, user);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -30,4 +30,12 @@ public interface ContentAnalyticsFactory {
*/
ReportResponse getRawReport(final CubeJSQuery query, final User user);

/**
* Runs the raw report based on the cube js json string query and user.
*
* @param cubeJsQueryJson the query to run the report.
* @param user the user to run the report.
* @return the report response.
*/
ReportResponse getRawReport(String cubeJsQueryJson, User user);
}
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,21 @@ public ReportResponse getRawReport(final CubeJSQuery cubeJSQuery, final User use
}
}

@Override
public ReportResponse getRawReport(final String cubeJsQueryJson, final User user) {

try {

Logger.debug(this, ()-> "Getting the report for the raw query: " + cubeJsQueryJson);
final CubeJSClient cubeClient = cubeJSClientFactory.create(user);
return toReportResponse(cubeClient.send(cubeJsQueryJson));
} catch (DotDataException| DotSecurityException e) {

Logger.error(this, e.getMessage(), e);
throw new DotRuntimeException(e);
}
}

private ReportResponse toReportResponse(final CubeJSResultSet cubeJSResultSet) {

return new ReportResponse(StreamSupport.stream(cubeJSResultSet.spliterator(), false).collect(Collectors.toList()));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
import com.dotcms.api.system.event.SystemEventType;
import com.dotcms.api.system.event.message.MessageSeverity;
import com.dotcms.api.system.event.message.SystemMessageEventUtil;
import com.dotcms.api.system.event.message.builder.SystemMessage;
import com.dotcms.api.system.event.message.builder.SystemMessageBuilder;
import com.dotcms.exception.AnalyticsException;
import com.dotcms.security.apps.AbstractProperty;
Expand All @@ -22,9 +23,11 @@
import com.dotmarketing.util.DateUtil;
import com.dotmarketing.util.Logger;
import com.liferay.portal.language.LanguageUtil;
import com.liferay.portal.model.User;
import io.vavr.control.Try;
import org.apache.commons.lang3.StringUtils;

import java.util.Collections;
import java.util.Objects;
import java.util.Optional;

Expand Down Expand Up @@ -99,6 +102,7 @@ public void notify(final AppSecretSavedEvent event) {
Logger.error(
this,
String.format("Cannot process event for app update due to: %s", e.getMessage()), e);
notifyErrorToTheUser(e, event);
}
});
});
Expand All @@ -112,6 +116,17 @@ public void notify(final AppSecretSavedEvent event) {
}
}

private void notifyErrorToTheUser(final AnalyticsException e, final AppSecretSavedEvent event) {

final SystemMessage systemMessage = new SystemMessageBuilder()
.setMessage(String.format("Cannot do the app update due to: %s", e.getMessage()))
.setSeverity(MessageSeverity.ERROR)
.setLife(DateUtil.TEN_SECOND_MILLIS)
.create();

SystemMessageEventUtil.getInstance().pushMessage(systemMessage, Collections.singletonList(event.getUserId()));
}

/**
* Pushes message to notify that even though analytics app env-var properties were changed though the Analytics
* App, those changes were ignored since they are govern by env-vars.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,15 +13,23 @@

import javax.enterprise.context.ApplicationScoped;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.List;
import java.util.Objects;
import java.util.stream.Collectors;
import java.util.stream.Stream;

import static com.liferay.util.StringPool.APOSTROPHE;
import static com.liferay.util.StringPool.BLANK;

/**
* Parser for the analytics query, it can parse a json string to a {@link AnalyticsQuery} or a {@link CubeJSQuery}
* This class exposes a parser for the {@link AnalyticsQuery} class. It can parse a JSON string
* into a {@link AnalyticsQuery} object or a {@link CubeJSQuery} object. The Analytics Query
* exposes a more readable and simple syntax compared to the CubeJS query
*
* @author jsanca
* @since Sep 19th, 2024
*/
@ApplicationScoped
public class AnalyticsQueryParser {
Expand All @@ -44,14 +52,14 @@ public class AnalyticsQueryParser {
public AnalyticsQuery parseJsonToQuery(final String json) {

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

Logger.debug(this, ()-> "Parsing json to query: " + json);
Logger.debug(this, ()-> "Parsing json query: " + json);
return DotObjectMapperProvider.getInstance().getDefaultObjectMapper()
.readValue(json, AnalyticsQuery.class);
} catch (JsonProcessingException e) {
} catch (final JsonProcessingException e) {
Logger.error(this, e.getMessage(), e);
throw new DotRuntimeException(e);
}
Expand Down Expand Up @@ -80,18 +88,20 @@ public CubeJSQuery parseJsonToCubeQuery(final String json) {
}

/**
* Parse an {@link AnalyticsQuery} to a {@link CubeJSQuery}
* @param query
* @return CubeJSQuery
* Parses an {@link AnalyticsQuery} object into a {@link CubeJSQuery} object, which represents
* the official CubeJS query.
*
* @param query The {@link AnalyticsQuery} object to be parsed.
*
* @return The {@link CubeJSQuery} object.
*/
public CubeJSQuery parseQueryToCubeQuery(final AnalyticsQuery query) {

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

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

if (UtilMethods.isSet(query.getDimensions())) {
builder.dimensions(query.getDimensions());
Expand Down Expand Up @@ -137,6 +147,15 @@ private Collection<CubeJSQuery.OrderItem> parseOrders(final String orders) {
).collect(Collectors.toList());
}

/**
* Parses the value of the {@code filters} attribute of the {@link AnalyticsQuery} object. This
* filter can have several logical operators, and be able to compare a variable against multiple
* values.
*
* @param filters the value of the {@code filters} attribute.
*
* @return A collection of {@link Filter} objects.
*/
private Collection<Filter> parseFilters(final String filters) {
final Tuple2<List<FilterParser.Token>,List<FilterParser.LogicalOperator>> result =
FilterParser.parseFilterExpression(filters);
Expand All @@ -145,14 +164,13 @@ private Collection<Filter> parseFilters(final String filters) {
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}));
parseOperator(token.operator),
this.parseTokenValues(token.values)));
}

// if has operators
// Are there any operators?
if (UtilMethods.isSet(result._2())) {

FilterParser.LogicalOperator logicalOperator = result._2().get(0); // first one
Expand All @@ -178,6 +196,21 @@ private Collection<Filter> parseFilters(final String filters) {
return filterList;
}

/**
* Takes the value of a token and parses its contents into an array of strings. A token can have
* both single and multiple values.
*
* @param values The value of the token.
*
* @return The value or values of a token as an array of strings.
*/
private String[] parseTokenValues(final String values) {
final String[] valueArray = values.split(",");
return Arrays.stream(valueArray).map(
value -> value.trim().replaceAll(APOSTROPHE, BLANK))
.toArray(String[]::new);
}

private SimpleFilter.Operator parseOperator(final String operator) {
switch (operator) {
case "=":
Expand All @@ -192,4 +225,5 @@ private SimpleFilter.Operator parseOperator(final String operator) {
throw new DotRuntimeException("Operator not supported: " + operator);
}
}

}
56 changes: 36 additions & 20 deletions dotCMS/src/main/java/com/dotcms/analytics/query/FilterParser.java
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
package com.dotcms.analytics.query;

import com.dotmarketing.util.Logger;
import com.dotmarketing.util.UtilMethods;
import io.vavr.Tuple;
import io.vavr.Tuple2;

Expand All @@ -9,23 +11,27 @@
import java.util.regex.Pattern;

/**
* Parser for a filter expression
* Example:
* This class exposes a parser for the {@code filters} attribute in our Analytics Query object. For
* example, the following expression:
* <pre>
* FilterParser.parseFilterExpression("Events.variant = ['B'] or Events.experiments = ['C']");
* {@code FilterParser.parseFilterExpression("Events.variant = ['B'] or Events.experiments = ['C']");}
* </pre>
* should return 2 tokens and 1 logical operator
* Tokens are member, operator and values (Events.variant, =, B) and the operator is 'and' or 'or'
* <p>Should return 2 tokens and 1 logical operator. Tokens are composed of a member, an operator,
* and one or more values.</p>
*
* @author jsanca
* @since Sep 19th, 2024
*/
public class FilterParser {

private static final String EXPRESSION_REGEX = "(\\w+\\.\\w+)\\s*(=|!=|in|!in)\\s*\\['(.*?)'";
private static final String EXPRESSION_REGEX = "(\\w+\\.\\w+)\\s*(=|!=|in|!in)\\s*\\[\\s*((?:'([^']*)'|\\d+)(?:\\s*,\\s*(?:'([^']*)'|\\d+))*)\\s*]";
private static final String LOGICAL_OPERATOR_REGEX = "\\s*(and|or)\\s*";

private static final Pattern TOKEN_PATTERN = Pattern.compile(EXPRESSION_REGEX);
private static final Pattern LOGICAL_PATTERN = Pattern.compile(LOGICAL_OPERATOR_REGEX);

static class Token {
public static class Token {

String member;
String operator;
String values;
Expand All @@ -37,37 +43,46 @@ public Token(final String member,
this.operator = operator;
this.values = values;
}

}

enum LogicalOperator {
public enum LogicalOperator {
AND,
OR,
UNKNOWN
}

/**
* This method parser the filter expression such as
* [Events.variant = [“B”] or Events.experiments = [“B”]]
* @param expression String
* @return return the token expression plus the logical operators
* Parses the value of the {@code filter} attribute, allowing both single and multiple values.
* For instance, the following expression:
* <pre>
* {@code request.whatAmI = ['PAGE','FILE'] and request.url in ['/blog']}
* </pre>
* <p>Allows you to retrieve results for both HTML Pages and Files whose URL contains the
* String {@code "/blog"}.</p>
*
* @param expression The value of the {@code filter} attribute.
*
* @return A {@link Tuple2} object containing token expression plus the logical operators.
*/
public static Tuple2<List<Token>,List<LogicalOperator>> parseFilterExpression(final String expression) {

final List<Token> tokens = new ArrayList<>();
final List<LogicalOperator> logicalOperators = new ArrayList<>();
// note:Need to use cache here
if (UtilMethods.isNotSet(expression)) {
return Tuple.of(tokens, logicalOperators);
}
// TODO: We need to use cache here
final Matcher tokenMatcher = TOKEN_PATTERN.matcher(expression);

// Extract the tokens (member, operator, values)
while (tokenMatcher.find()) {
final String member = tokenMatcher.group(1); // Example: Events.variant
final String operator = tokenMatcher.group(2); // Example: =
final String values = tokenMatcher.group(3); // Example: "B"
final String values = tokenMatcher.group(3); // Example: "'B'", or multiple values such as "'A', 'B'"
tokens.add(new Token(member, operator, values));
}

// Pattern for logical operators (and, or)
// Need to use cache here
// TODO: Need to use cache here
final Matcher logicalMatcher = LOGICAL_PATTERN.matcher(expression);

// Extract logical operators
Expand All @@ -76,9 +91,9 @@ public static Tuple2<List<Token>,List<LogicalOperator>> parseFilterExpression(fi
logicalOperators.add(parseLogicalOperator(logicalOperator));
}

// if any unknown should fails
// note: should validate logical operators should be length - 1 of the tokens???

if (tokens.isEmpty() && logicalOperators.isEmpty()) {
Logger.warn(FilterParser.class, String.format("Filter expression failed to be parsed: %s", expression));
}
return Tuple.of(tokens, logicalOperators);
}

Expand All @@ -93,4 +108,5 @@ private static LogicalOperator parseLogicalOperator(final String logicalOperator
return LogicalOperator.UNKNOWN;
}
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,7 @@ private void fireCollectorsAndEmitEvent(final HttpServletRequest request,
.collect(java.util.stream.Collectors.toList());
}));
} catch (Exception e) {
Logger.debug(WebEventsCollectorServiceFactory.class, () -> "Error saving Analitycs Events:" + e.getMessage());
Logger.debug(WebEventsCollectorServiceFactory.class, () -> "Error saving Analytics Events:" + e.getMessage());
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,9 @@ public class SystemTableInitializer implements DotInitializer {

@Override
public void init() {
Config.initSystemTableConfigSource();
if (!Config.isSystemTableConfigSourceInit()) {
Config.initSystemTableConfigSource();
}
// Load the all system table into the system cache
APILocator.getSystemAPI().getSystemTable().all();
}
Expand Down
Loading

0 comments on commit 97cf17c

Please sign in to comment.