Skip to content

Commit

Permalink
Merge pull request #1209 from gooddata/afmx/rollup-totals
Browse files Browse the repository at this point in the history
Introduce totals support in visualizations and their conversions
peter-plochan authored Jan 7, 2025
2 parents 7080734 + caa39b0 commit 9c5f1d0
Showing 9 changed files with 730 additions and 26 deletions.
Original file line number Diff line number Diff line change
@@ -11,8 +11,10 @@
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.gooddata.sdk.model.executeafm.afm.LocallyIdentifiable;
import com.gooddata.sdk.model.executeafm.resultspec.TotalItem;

import java.io.Serializable;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;

@@ -26,17 +28,33 @@ public class Bucket implements Serializable, LocallyIdentifiable {
private static final long serialVersionUID = -7718720886547680021L;
private final String localIdentifier;
private final List<BucketItem> items;
private final List<TotalItem> totals;

/**
* Creates new instance of bucket without totals
*
* @param localIdentifier local identifier of bucket
* @param items list of {@link BucketItem}s for this bucket
*/
public Bucket(@JsonProperty("localIdentifier") final String localIdentifier,
@JsonProperty("items") final List<BucketItem> items) {
this(localIdentifier, items, null);
}

/**
* Creates new instance of bucket
*
* @param localIdentifier local identifier of bucket
* @param items list of {@link BucketItem}s for this bucket
* @param items list of {@link BucketItem}s for this bucket
* @param totals list of {@link TotalItem}s for this bucket
*/
@JsonCreator
public Bucket(@JsonProperty("localIdentifier") final String localIdentifier,
@JsonProperty("items") final List<BucketItem> items) {
@JsonProperty("items") final List<BucketItem> items,
@JsonProperty("totals") List<TotalItem> totals) {
this.localIdentifier = localIdentifier;
this.items = items;
this.totals = totals;
}

/**
@@ -53,6 +71,13 @@ public List<BucketItem> getItems() {
return items;
}

/**
* @return list of defined {@link TotalItem}s
*/
public List<TotalItem> getTotals() {
return totals;
}

@JsonIgnore
VisualizationAttribute getOnlyAttribute() {
if (getItems() != null && getItems().size() == 1) {
@@ -67,15 +92,18 @@ VisualizationAttribute getOnlyAttribute() {

@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
if (this == o)
return true;
if (o == null || getClass() != o.getClass())
return false;
Bucket bucket = (Bucket) o;
return Objects.equals(localIdentifier, bucket.localIdentifier) &&
Objects.equals(items, bucket.items);
return Objects.equals(localIdentifier, bucket.localIdentifier)
&& Objects.equals(items, bucket.items)
&& Objects.equals(totals, bucket.totals);
}

@Override
public int hashCode() {
return Objects.hash(localIdentifier, items);
return Objects.hash(localIdentifier, items, totals);
}
}
Original file line number Diff line number Diff line change
@@ -11,6 +11,7 @@
import com.gooddata.sdk.model.executeafm.Execution;
import com.gooddata.sdk.model.executeafm.afm.Afm;
import com.gooddata.sdk.model.executeafm.afm.AttributeItem;
import com.gooddata.sdk.model.executeafm.afm.NativeTotalItem;
import com.gooddata.sdk.model.executeafm.afm.filter.CompatibilityFilter;
import com.gooddata.sdk.model.executeafm.afm.filter.DateFilter;
import com.gooddata.sdk.model.executeafm.afm.filter.ExtendedFilter;
@@ -24,8 +25,11 @@
import com.gooddata.sdk.model.executeafm.resultspec.Dimension;
import com.gooddata.sdk.model.executeafm.resultspec.ResultSpec;
import com.gooddata.sdk.model.executeafm.resultspec.SortItem;
import com.gooddata.sdk.model.executeafm.resultspec.TotalItem;
import com.gooddata.sdk.model.md.report.Total;

import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.function.Function;
import java.util.stream.Collectors;
@@ -43,9 +47,12 @@ public abstract class VisualizationConverter {

/**
* Generate Execution from Visualization object.
* <p>
* <b>NOTE: totals are not included in this conversion</b>
*
* @param visualizationObject which will be converted to {@link Execution}
* @param visualizationClassGetter {@link Function} for fetching VisualizationClass, which is necessary for correct generation of {@link ResultSpec}
* @param visualizationClassGetter {@link Function} for fetching VisualizationClass,
* which is necessary for correct generation of {@link ResultSpec}
* @return {@link Execution} object
* @see #convertToExecution(VisualizationObject, VisualizationClass)
*/
@@ -59,6 +66,8 @@ public static Execution convertToExecution(final VisualizationObject visualizati

/**
* Generate Execution from Visualization object.
* <p>
* <b>NOTE: totals are not included in this conversion</b>
*
* @param visualizationObject which will be converted to {@link Execution}
* @param visualizationClass visualizationClass, which is necessary for correct generation of {@link ResultSpec}
@@ -75,27 +84,80 @@ public static Execution convertToExecution(final VisualizationObject visualizati
return new Execution(afm, resultSpec);
}

/**
* Generate Execution from Visualization object with totals included.
*
* @param visualizationObject which will be converted to {@link Execution}
* @param visualizationClassGetter {@link Function} for fetching VisualizationClass,
* which is necessary for correct generation of {@link ResultSpec}
* @return {@link Execution} object
* @see #convertToExecutionWithTotals(VisualizationObject, VisualizationClass)
*/
public static Execution convertToExecutionWithTotals(final VisualizationObject visualizationObject,
final Function<String, VisualizationClass> visualizationClassGetter) {
notNull(visualizationObject, "visualizationObject");
notNull(visualizationClassGetter, "visualizationClassGetter");
return convertToExecutionWithTotals(visualizationObject,
visualizationClassGetter.apply(visualizationObject.getVisualizationClassUri()));
}

/**
* Generate Execution from Visualization object with totals included.
*
* @param visualizationObject which will be converted to {@link Execution}
* @param visualizationClass visualizationClass, which is necessary for correct generation of {@link ResultSpec}
* @return {@link Execution} object
* @see #convertToAfmWithNativeTotals(VisualizationObject)
* @see #convertToResultSpecWithTotals(VisualizationObject, VisualizationClass)
*/
public static Execution convertToExecutionWithTotals(final VisualizationObject visualizationObject,
final VisualizationClass visualizationClass) {
notNull(visualizationObject, "visualizationObject");
notNull(visualizationClass, "visualizationClass");
ResultSpec resultSpec = convertToResultSpecWithTotals(visualizationObject, visualizationClass);
Afm afm = convertToAfmWithNativeTotals(visualizationObject);
return new Execution(afm, resultSpec);
}

/**
* Generate Afm from Visualization object.
* <p>
* <b>NOTE: native totals are not included in this conversion</b>
*
* @param visualizationObject which will be converted to {@link Execution}
* @return {@link Afm} object
*/
public static Afm convertToAfm(final VisualizationObject visualizationObject) {
notNull(visualizationObject, "visualizationObject");
final VisualizationObject visualizationObjectWithoutTotals = removeTotals(visualizationObject);
return convertToAfmWithNativeTotals(visualizationObjectWithoutTotals);
}

/**
* Generate Afm from Visualization object with native totals included.
*
* @param visualizationObject which will be converted to {@link Execution}
* @return {@link Afm} object
*/
public static Afm convertToAfmWithNativeTotals(final VisualizationObject visualizationObject) {
notNull(visualizationObject, "visualizationObject");
final List<AttributeItem> attributes = convertAttributes(visualizationObject.getAttributes());
final List<CompatibilityFilter> filters = convertFilters(visualizationObject.getFilters());
final List<MeasureItem> measures = convertMeasures(visualizationObject.getMeasures());
final List<NativeTotalItem> totals = convertNativeTotals(visualizationObject);

return new Afm(attributes, filters, measures, null);
return new Afm(attributes, filters, measures, totals);
}

/**
* Generate ResultSpec from Visualization object. Currently {@link ResultSpec}'s {@link Dimension}s can be generated
* for table and four types of chart: bar, column, line and pie.
* <p>
* <b>NOTE: totals are not included in this conversion</b>
*
* @param visualizationObject which will be converted to {@link Execution}
* @param visualizationClassGetter {@link Function} for fetching VisualizationClass, which is necessary for correct generation of {@link ResultSpec}
* @param visualizationClassGetter {@link Function} for fetching VisualizationClass,
* which is necessary for correct generation of {@link ResultSpec}
* @return {@link Execution} object
*/
public static ResultSpec convertToResultSpec(final VisualizationObject visualizationObject,
@@ -109,6 +171,8 @@ public static ResultSpec convertToResultSpec(final VisualizationObject visualiza
/**
* Generate ResultSpec from Visualization object. Currently {@link ResultSpec}'s {@link Dimension}s can be generated
* for table and four types of chart: bar, column, line and pie.
* <p>
* <b>NOTE: totals are not included in this conversion</b>
*
* @param visualizationObject which will be converted to {@link Execution}
* @param visualizationClass VisualizationClass, which is necessary for correct generation of {@link ResultSpec}
@@ -118,6 +182,39 @@ public static ResultSpec convertToResultSpec(final VisualizationObject visualiza
final VisualizationClass visualizationClass) {
notNull(visualizationObject, "visualizationObject");
notNull(visualizationClass, "visualizationClass");
final VisualizationObject visualizationObjectWithoutTotals = removeTotals(visualizationObject);
return convertToResultSpecWithTotals(visualizationObjectWithoutTotals, visualizationClass);
}

/**
* Generate ResultSpec from Visualization object with totals included. Currently {@link ResultSpec}'s {@link Dimension}s
* can be generated for table and four types of chart: bar, column, line and pie.
*
* @param visualizationObject which will be converted to {@link Execution}
* @param visualizationClassGetter {@link Function} for fetching VisualizationClass,
* which is necessary for correct generation of {@link ResultSpec}
* @return {@link Execution} object
*/
public static ResultSpec convertToResultSpecWithTotals(final VisualizationObject visualizationObject,
final Function<String, VisualizationClass> visualizationClassGetter) {
notNull(visualizationObject, "visualizationObject");
notNull(visualizationClassGetter, "visualizationClassGetter");
return convertToResultSpecWithTotals(visualizationObject,
visualizationClassGetter.apply(visualizationObject.getVisualizationClassUri()));
}

/**
* Generate ResultSpec from Visualization object with totals included. Currently {@link ResultSpec}'s {@link Dimension}s
* can be generated for table and four types of chart: bar, column, line and pie.
*
* @param visualizationObject which will be converted to {@link Execution}
* @param visualizationClass VisualizationClass, which is necessary for correct generation of {@link ResultSpec}
* @return {@link Execution} object
*/
public static ResultSpec convertToResultSpecWithTotals(final VisualizationObject visualizationObject,
final VisualizationClass visualizationClass) {
notNull(visualizationObject, "visualizationObject");
notNull(visualizationClass, "visualizationClass");
isTrue(visualizationObject.getVisualizationClassUri().equals(visualizationClass.getUri()),
"visualizationClass URI does not match the URI within visualizationObject, "
+ "you're trying to create ResultSpec for incompatible objects");
@@ -144,6 +241,21 @@ static List<SortItem> parseSorting(final String properties) throws Exception {
return MAPPER.convertValue(nodeSortItems, mapType);
}

/**
* Creates a new {@link VisualizationObject} derived from the original one, with all "totals" removed from its buckets.
* This is to ensure backward compatibility in cases where totals were not previously handled.
*
* @param visualizationObject original {@link VisualizationObject}
* @return a new VisualizationObject derived from the original but without any totals in the buckets.
*/
private static VisualizationObject removeTotals(final VisualizationObject visualizationObject) {
final List<Bucket> bucketsWithoutTotals = visualizationObject.getBuckets().stream()
// create buckets without totals
.map(bucket -> new Bucket(bucket.getLocalIdentifier(), bucket.getItems()))
.collect(toList());
return visualizationObject.withBuckets(bucketsWithoutTotals);
}

private static List<Dimension> getDimensions(final VisualizationObject visualizationObject,
final VisualizationType visualizationType) {
switch (visualizationType) {
@@ -216,12 +328,16 @@ private static List<Dimension> getDimensionsForTable(final VisualizationObject v
List<Dimension> dimensions = new ArrayList<>();

List<VisualizationAttribute> attributes = visualizationObject.getAttributes();
List<TotalItem> totals = visualizationObject.getTotals();

if (!attributes.isEmpty()) {
dimensions.add(new Dimension(attributes.stream()
final Dimension attributeDimension = new Dimension(attributes.stream()
.map(VisualizationAttribute::getLocalIdentifier)
.collect(toList())
));
.collect(toList()));
if (!totals.isEmpty()) {
attributeDimension.setTotals(new HashSet<>(totals));
}
dimensions.add(attributeDimension);
} else {
dimensions.add(new Dimension(new ArrayList<>()));
}
@@ -316,4 +432,43 @@ private static <T> List<T> removeIrrelevantFilters(final List<T> filters) {
})
.collect(Collectors.toList());
}

private static List<NativeTotalItem> convertNativeTotals(final VisualizationObject visualizationObject) {
final List<Bucket> attributeBuckets = getAttributeBuckets(visualizationObject);
final List<String> attributeIds = getIdsFromAttributeBuckets(attributeBuckets);
return attributeBuckets.stream()
.filter(bucket -> bucket.getTotals() != null)
.flatMap(bucket -> bucket.getTotals().stream())
.filter(totalItem -> isNativeTotal(totalItem) && attributeIds.contains(totalItem.getAttributeIdentifier()))
.map(totalItem -> convertToNativeTotalItem(totalItem, attributeIds))
.collect(toList());
}

private static NativeTotalItem convertToNativeTotalItem(TotalItem totalItem, List<String> attributeIds) {
final int attributeIdx = attributeIds.indexOf(totalItem.getAttributeIdentifier());
return new NativeTotalItem(
totalItem.getMeasureIdentifier(),
new ArrayList<>(attributeIds.subList(0, attributeIdx))
);
}

private static List<Bucket> getAttributeBuckets(final VisualizationObject visualizationObject) {
return visualizationObject.getBuckets().stream()
.filter(bucket -> bucket.getItems().stream().allMatch(AttributeItem.class::isInstance))
.collect(toList());
}

private static List<String> getIdsFromAttributeBuckets(final List<Bucket> attributeBuckets) {
return attributeBuckets.stream()
.flatMap(bucket ->
bucket.getItems().stream()
.map(AttributeItem.class::cast)
.map(AttributeItem::getLocalIdentifier)
)
.collect(toList());
}

private static boolean isNativeTotal(TotalItem totalItem) {
return totalItem.getType() != null && Total.NAT.name().equals(totalItem.getType().toUpperCase());
}
}
Loading

0 comments on commit 9c5f1d0

Please sign in to comment.