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

Introduce totals support in visualizations and their conversions #1209

Merged
merged 2 commits into from
Jan 7, 2025
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 @@ -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;

Expand All @@ -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;
}

/**
Expand All @@ -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) {
Expand All @@ -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
Expand Up @@ -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;
Expand All @@ -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;
Expand All @@ -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)
*/
Expand All @@ -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}
Expand All @@ -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,
Expand All @@ -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}
Expand All @@ -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");
Expand All @@ -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) {
Expand Down Expand Up @@ -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<>()));
}
Expand Down Expand Up @@ -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
Loading