Skip to content

Commit

Permalink
Creating Util classes to build a CubeJS Query and sent a Request to a… (
Browse files Browse the repository at this point in the history
#23895)

* Creating Util classes to build a CubeJS Query and sent a Request to a CubeJs Server

* Feedback
  • Loading branch information
freddyDOTCMS committed Jan 27, 2023
1 parent a9bfca3 commit aa04d26
Show file tree
Hide file tree
Showing 11 changed files with 1,661 additions and 4 deletions.
101 changes: 101 additions & 0 deletions dotCMS/src/main/java/com/dotcms/cube/CubeJSClient.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
package com.dotcms.cube;

import static com.dotcms.util.CollectionsUtils.map;

import com.dotcms.http.CircuitBreakerUrl;
import com.dotcms.http.CircuitBreakerUrl.Method;
import com.dotcms.http.CircuitBreakerUrl.Response;
import com.dotcms.jitsu.EventLogRunnable;
import com.dotcms.util.DotPreconditions;
import com.dotcms.util.JsonUtil;
import com.dotmarketing.util.Logger;
import com.dotmarketing.util.UtilMethods;
import com.liferay.util.StringPool;
import io.vavr.control.Try;
import java.io.IOException;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

/**
* CubeJS Client it allow to send a Request to a Cube JS Server.
* Example:
*
* <code>
*
* final String cubeServerIp = "127.0.0.1";
* final int cubeJsServerPort = 5000;
*
* final CubeJSQuery cubeJSQuery = new Builder()
* .dimensions("Events.experiment", "Events.variant")
* .build();
*
* final CubeClient cubeClient = new CubeClient(String.format("http://%s:%s", cubeServerIp, cubeJsServerPort));
* final CubeJSResultSet cubeJSResultSet = cubeClient.send(cubeJSQuery);
* </code>
*/
public class CubeJSClient {
private String url;

public CubeJSClient(final String url) {
this.url = url;
}

/**
* Send a request to a CubeJS Server.
*
* Example:
*
* <code>
*
* final String cubeServerIp = "127.0.0.1";
* final int cubeJsServerPort = 5000;
*
* final CubeJSQuery cubeJSQuery = new Builder()
* .dimensions("Events.experiment", "Events.variant", "Events.utcTime")
* .build();
*
* final CubeClient cubeClient = new CubeClient(String.format("http://%s:%s", cubeServerIp, cubeJsServerPort));
* final CubeJSResultSet cubeJSResultSet = cubeClient.send(cubeJSQuery);
*
* for (ResultSetItem resultSetItem : cubeJSResultSet) {
* System.out.println("Events.experiment", resultSetItem.get("Events.experiment").get())
* System.out.println("Events.variant", resultSetItem.get("Events.variant").get())
* System.out.println("Events.utcTime", resultSetItem.get("Events.utcTime").get())
* }
* </code>
*
* @param query Query to be run in the CubeJS Server
* @return
*/
public CubeJSResultSet send(final CubeJSQuery query) {

DotPreconditions.notNull(query, "Query not must be NULL");

final CircuitBreakerUrl cubeJSClient = CircuitBreakerUrl.builder()
.setMethod(Method.GET)
.setUrl(String.format("%s/cubejs-api/v1/load", url))
.setParams(map("query", query.toString()))
.setTimeout(4000)
.build();

final Response<String> response = Try.of(cubeJSClient::doResponse)
.onFailure(e -> Logger.warnAndDebug(EventLogRunnable.class, e.getMessage(), e))
.getOrElse(CircuitBreakerUrl.EMPTY_RESPONSE);

try {
final String responseAsString = UtilMethods.isSet(response) ? response.getResponse() :
StringPool.BLANK;
final Map<String, Object> responseAsMap = UtilMethods.isSet(responseAsString) ?
JsonUtil.getJsonFromString(responseAsString) : new HashMap<>();
final List<Map<String, Object>> data = (List<Map<String, Object>>) responseAsMap.get("data");

return new CubeJSResultSet(UtilMethods.isSet(data) ? data : Collections.emptyList());
} catch (IOException e) {
throw new RuntimeException(e);
}


}
}
182 changes: 182 additions & 0 deletions dotCMS/src/main/java/com/dotcms/cube/CubeJSQuery.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,182 @@
package com.dotcms.cube;



import com.dotcms.cube.filters.Filter.Order;
import com.dotcms.cube.filters.LogicalFilter;
import com.dotcms.cube.filters.SimpleFilter;
import com.dotcms.cube.filters.SimpleFilter.Operator;
import com.dotcms.cube.filters.Filter;
import com.dotcms.util.JsonUtil;
import com.dotmarketing.util.UtilMethods;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
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>
* final CubeJSQuery cubeJSQuery = new Builder()
* .dimensions("Events.experiment")
* .measures("Events.count")
* .filter("Events.variant", SimpleFilter.Operator.EQUALS, "B")
* .build();
* </code>
*
* To get:
*
* <code>
* {
* "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>
*/
public class CubeJSQuery {

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

private OrderItem[] orders;

private CubeJSQuery(final String[] dimensions,
final String[] measures,
final Filter[] filters,
final OrderItem[] orderItems) {

this.dimensions = dimensions;
this.measures = measures;
this.filters = filters;
this.orders = orderItems;
}

@Override
public String toString() {
try {
return JsonUtil.getJsonAsString(getMap());
} catch (IOException e) {
throw new RuntimeException(e);
}
}

private Map<String, Object> getMap() {
final Map<String, Object> map = new HashMap<>();

if (UtilMethods.isSet(dimensions)) {
map.put("dimensions", dimensions);
}

if (UtilMethods.isSet(measures)) {
map.put("measures", measures);
}

if (filters.length > 0) {
map.put("filters", getFiltersAsMap());
}

if (orders.length > 0) {
map.put("order", getOrdersAsMap());
}

return map;
}

private Map<String, String> getOrdersAsMap() {
final Map<String, String> resultMap = new HashMap();

for (final OrderItem order : orders) {
resultMap.put(order.orderBy, order.order.name().toLowerCase());
}

return resultMap;
}
private List<Map<String, Object>> getFiltersAsMap() {
return Arrays.stream(filters)
.map(filter -> filter.asMap())
.collect(Collectors.toList());
}

public static class Builder {
private String[] dimensions;
private String[] measures;
private List<Filter> filters = new ArrayList<>();
private List<OrderItem> orders = new ArrayList<>();

public CubeJSQuery build(){
if (!UtilMethods.isSet(dimensions) && !UtilMethods.isSet(measures)) {
throw new IllegalStateException("Must set dimensions or measures");
}

return new CubeJSQuery(dimensions, measures,
filters.toArray(new Filter[filters.size()]),
orders.toArray(new OrderItem[orders.size()]));
}

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

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

public Builder filter(final String member, Operator operator, final String... values) {
filters.add(new SimpleFilter(member, operator, values));
return this;
}

public Builder filter(final LogicalFilter logicalFilter) {
filters.add(logicalFilter);
return this;
}

public Builder order(final String orderBy, final Order order) {
orders.add(new OrderItem(orderBy, order));
return this;
}
}

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

public OrderItem(final String orderBy, final Order order) {
this.orderBy = orderBy;
this.order = order;
}

public String getOrderBy() {
return orderBy;
}

public Order getOrder() {
return order;
}
}
}
54 changes: 54 additions & 0 deletions dotCMS/src/main/java/com/dotcms/cube/CubeJSResultSet.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
package com.dotcms.cube;

import com.dotcms.cube.CubeJSResultSet.ResultSetItem;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Spliterator;
import java.util.function.Consumer;
import java.util.stream.Collectors;
import org.jetbrains.annotations.NotNull;

/**
* Represent a Result from running a CubeJS Query in a CubeJS Server.
*/
public class CubeJSResultSet implements Iterable<ResultSetItem> {
private List<ResultSetItem> data;
public CubeJSResultSet(final List<Map<String, Object>> data){
this.data = data.stream().map(map -> new ResultSetItem(map)).collect(Collectors.toList());
}

public int size() {
return data.size();
}

@NotNull
@Override
public Iterator<ResultSetItem> iterator() {
return data.iterator();
}

@Override
public void forEach(Consumer<? super ResultSetItem> action) {
Iterable.super.forEach(action);
}

@Override
public Spliterator<ResultSetItem> spliterator() {
return Iterable.super.spliterator();
}

public static class ResultSetItem {

private Map<String, Object> item;
ResultSetItem(final Map<String, Object> item) {
this.item = item;
}

public Optional<Object> get(final String name){
return Optional.ofNullable(item.get(name));
}
}

}
21 changes: 21 additions & 0 deletions dotCMS/src/main/java/com/dotcms/cube/filters/Filter.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package com.dotcms.cube.filters;

import java.util.Map;

/**
* Represents a CubeJs Query Filter
*
* @see <a href="https://cube.dev/docs/query-format#filters-formate">Filters format</a>
*/
public interface Filter {

static LogicalFilter.Builder and(){
return null;
}

Map<String, Object> asMap();

public enum Order {
ASC, DESC;
}
}

0 comments on commit aa04d26

Please sign in to comment.