diff --git a/gooddata-java-model/src/main/java/com/gooddata/sdk/model/md/visualization/Bucket.java b/gooddata-java-model/src/main/java/com/gooddata/sdk/model/md/visualization/Bucket.java index e4a07c2b1..83d81a4bb 100644 --- a/gooddata-java-model/src/main/java/com/gooddata/sdk/model/md/visualization/Bucket.java +++ b/gooddata-java-model/src/main/java/com/gooddata/sdk/model/md/visualization/Bucket.java @@ -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 items; + private final List 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 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 items) { + @JsonProperty("items") final List items, + @JsonProperty("totals") List totals) { this.localIdentifier = localIdentifier; this.items = items; + this.totals = totals; } /** @@ -53,6 +71,13 @@ public List getItems() { return items; } + /** + * @return list of defined {@link TotalItem}s + */ + public List 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); } } diff --git a/gooddata-java-model/src/main/java/com/gooddata/sdk/model/md/visualization/VisualizationConverter.java b/gooddata-java-model/src/main/java/com/gooddata/sdk/model/md/visualization/VisualizationConverter.java index c5598f6bc..73397793f 100644 --- a/gooddata-java-model/src/main/java/com/gooddata/sdk/model/md/visualization/VisualizationConverter.java +++ b/gooddata-java-model/src/main/java/com/gooddata/sdk/model/md/visualization/VisualizationConverter.java @@ -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. + *

+ * NOTE: totals are not included in this conversion * * @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. + *

+ * NOTE: totals are not included in this conversion * * @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 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. + *

+ * NOTE: native totals are not included in this conversion * * @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 attributes = convertAttributes(visualizationObject.getAttributes()); final List filters = convertFilters(visualizationObject.getFilters()); final List measures = convertMeasures(visualizationObject.getMeasures()); + final List 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. + *

+ * NOTE: totals are not included in this conversion * * @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. + *

+ * NOTE: totals are not included in this conversion * * @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 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 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 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 getDimensions(final VisualizationObject visualizationObject, final VisualizationType visualizationType) { switch (visualizationType) { @@ -216,12 +328,16 @@ private static List getDimensionsForTable(final VisualizationObject v List dimensions = new ArrayList<>(); List attributes = visualizationObject.getAttributes(); + List 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 List removeIrrelevantFilters(final List filters) { }) .collect(Collectors.toList()); } + + private static List convertNativeTotals(final VisualizationObject visualizationObject) { + final List attributeBuckets = getAttributeBuckets(visualizationObject); + final List 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 attributeIds) { + final int attributeIdx = attributeIds.indexOf(totalItem.getAttributeIdentifier()); + return new NativeTotalItem( + totalItem.getMeasureIdentifier(), + new ArrayList<>(attributeIds.subList(0, attributeIdx)) + ); + } + + private static List getAttributeBuckets(final VisualizationObject visualizationObject) { + return visualizationObject.getBuckets().stream() + .filter(bucket -> bucket.getItems().stream().allMatch(AttributeItem.class::isInstance)) + .collect(toList()); + } + + private static List getIdsFromAttributeBuckets(final List 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()); + } } diff --git a/gooddata-java-model/src/main/java/com/gooddata/sdk/model/md/visualization/VisualizationObject.java b/gooddata-java-model/src/main/java/com/gooddata/sdk/model/md/visualization/VisualizationObject.java index 1526c50dd..6b73969c7 100644 --- a/gooddata-java-model/src/main/java/com/gooddata/sdk/model/md/visualization/VisualizationObject.java +++ b/gooddata-java-model/src/main/java/com/gooddata/sdk/model/md/visualization/VisualizationObject.java @@ -24,6 +24,7 @@ import com.gooddata.sdk.model.executeafm.Execution; import com.gooddata.sdk.model.executeafm.afm.filter.ExtendedFilter; import com.gooddata.sdk.model.executeafm.resultspec.ResultSpec; +import com.gooddata.sdk.model.executeafm.resultspec.TotalItem; import com.gooddata.sdk.model.md.AbstractObj; import com.gooddata.sdk.model.md.Meta; import com.gooddata.sdk.model.md.Queryable; @@ -32,6 +33,7 @@ import java.io.Serializable; import java.util.ArrayList; +import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.function.Function; @@ -77,6 +79,14 @@ public List getMeasures() { return content.getMeasures(); } + /** + * @return all totals from all buckets in visualization object + */ + @JsonIgnore + public List getTotals() { + return content.getTotals(); + } + /** * Get measure by local identifier or null if not found * @param localIdentifier of measure @@ -216,6 +226,14 @@ public void setBuckets(List buckets) { content.setBuckets(buckets); } + /** + * @return a new copy of this {@link VisualizationObject} with the specified buckets + */ + @JsonIgnore + public VisualizationObject withBuckets(List buckets) { + return new VisualizationObject(content.withBuckets(buckets), meta); + } + /** * @return filters from visualization object */ @@ -391,6 +409,14 @@ public List getMeasures() { .collect(toList()); } + @JsonIgnore + public List getTotals() { + return buckets.stream() + .filter(bucket -> bucket.getTotals() != null) + .flatMap(bucket -> bucket.getTotals().stream()) + .collect(toList()); + } + @JsonIgnore public String getVisualizationClassUri() { return visualizationClass.getUri(); @@ -416,5 +442,19 @@ public UriObjQualifier getVisualizationClass() { public Map getReferenceItems() { return referenceItems; } + + /** + * @return a new copy of this {@link Content} with the specified buckets + */ + @JsonIgnore + public Content withBuckets(List buckets) { + return new Content( + visualizationClass, + buckets, + filters != null ? new ArrayList<>(filters) : null, + properties, + referenceItems != null ? new HashMap<>(referenceItems) : null + ); + } } } diff --git a/gooddata-java-model/src/test/groovy/com/gooddata/sdk/model/md/visualization/BucketTest.groovy b/gooddata-java-model/src/test/groovy/com/gooddata/sdk/model/md/visualization/BucketTest.groovy index 3c19b351c..05bdd6cca 100644 --- a/gooddata-java-model/src/test/groovy/com/gooddata/sdk/model/md/visualization/BucketTest.groovy +++ b/gooddata-java-model/src/test/groovy/com/gooddata/sdk/model/md/visualization/BucketTest.groovy @@ -6,6 +6,8 @@ package com.gooddata.sdk.model.md.visualization import com.gooddata.sdk.model.executeafm.UriObjQualifier +import com.gooddata.sdk.model.executeafm.resultspec.TotalItem +import com.gooddata.sdk.model.md.report.Total import nl.jqno.equalsverifier.EqualsVerifier import org.apache.commons.lang3.SerializationUtils import spock.lang.Shared @@ -18,6 +20,7 @@ import static spock.util.matcher.HamcrestSupport.that class BucketTest extends Specification { private static final String NO_ITEMS_BUCKET = "md/visualization/noItemsBucket.json" private static final String MIXED_BUCKET = "md/visualization/mixedBucket.json" + private static final String MIXED_BUCKET_WITH_TOTALS = "md/visualization/mixedBucketWithTotals.json" private static final String ATTRIBUTE_BUCKET = "md/visualization/attributeBucket.json" private static final String MEASURE_BUCKET = "md/visualization/measureBucket.json" private static final String MULTIPLE_ATTRIBUTES_BUCKET = "md/visualization/multipleAttributesBucket.json" @@ -32,19 +35,41 @@ class BucketTest extends Specification { that new Bucket("noItems", new ArrayList()), jsonEquals(noItemsBucket) } - @SuppressWarnings("GrDeprecatedAPIUsage") def "should serialize full"() { expect: - that new Bucket("attributeBucket", [ - new VisualizationAttribute(new UriObjQualifier("/uri/to/displayForm"), "attribute", "Attribute Alias"), - new Measure( - new VOSimpleMeasureDefinition(new UriObjQualifier("/uri/to/measure"), "sum", false, []), - "measure", - "Measure Alias", - "Measure", - null - ) - ]), jsonEquals(mixedBucket) + that new Bucket( + "attributeBucket", + [ + new VisualizationAttribute(new UriObjQualifier("/uri/to/displayForm"), "attribute", "Attribute Alias"), + new Measure( + new VOSimpleMeasureDefinition(new UriObjQualifier("/uri/to/measure"), "sum", false, []), + "measure", + "Measure Alias", + "Measure", + null + ) + ] as List + ), jsonEquals(mixedBucket) + } + + def "should serialize full with totals"() { + expect: + that new Bucket( + "attributeBucket", + [ + new VisualizationAttribute(new UriObjQualifier("/uri/to/displayForm"), "attribute", "Attribute Alias"), + new Measure( + new VOSimpleMeasureDefinition(new UriObjQualifier("/uri/to/measure"), "sum", false, []), + "measure", + "Measure Alias", + "Measure", + null + ) + ] as List, + [ + new TotalItem("measure", Total.NAT, "attribute") + ] + ), jsonEquals(readObjectFromResource("/$MIXED_BUCKET_WITH_TOTALS", Bucket.class)) } def "should return only attribute from bucket"() { diff --git a/gooddata-java-model/src/test/groovy/com/gooddata/sdk/model/md/visualization/VisualizationConverterTest.groovy b/gooddata-java-model/src/test/groovy/com/gooddata/sdk/model/md/visualization/VisualizationConverterTest.groovy index 08ac1d227..49dcf6529 100644 --- a/gooddata-java-model/src/test/groovy/com/gooddata/sdk/model/md/visualization/VisualizationConverterTest.groovy +++ b/gooddata-java-model/src/test/groovy/com/gooddata/sdk/model/md/visualization/VisualizationConverterTest.groovy @@ -6,6 +6,8 @@ package com.gooddata.sdk.model.md.visualization import com.fasterxml.jackson.core.JsonParseException +import com.fasterxml.jackson.databind.ObjectMapper +import com.gooddata.sdk.model.executeafm.Execution import com.gooddata.sdk.model.executeafm.LocalIdentifierQualifier import com.gooddata.sdk.model.executeafm.UriObjQualifier import com.gooddata.sdk.model.executeafm.afm.filter.AbsoluteDateFilter @@ -26,6 +28,7 @@ import com.gooddata.sdk.model.executeafm.resultspec.MeasureLocatorItem import com.gooddata.sdk.model.executeafm.resultspec.MeasureSortItem 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 spock.lang.Specification import spock.lang.Unroll @@ -35,9 +38,12 @@ import java.util.function.Function import static VisualizationConverter.convertToAfm import static VisualizationConverter.convertToResultSpec import static VisualizationConverter.parseSorting +import static com.gooddata.sdk.model.md.visualization.VisualizationConverter.convertToAfmWithNativeTotals import static com.gooddata.sdk.model.md.visualization.VisualizationConverter.convertToExecution import static com.gooddata.sdk.common.util.ResourceUtils.readObjectFromResource +import static com.gooddata.sdk.model.md.visualization.VisualizationConverter.convertToResultSpecWithTotals import static net.javacrumbs.jsonunit.JsonMatchers.jsonEquals +import static spock.util.matcher.HamcrestSupport.expect import static spock.util.matcher.HamcrestSupport.that class VisualizationConverterTest extends Specification { @@ -48,6 +54,9 @@ class VisualizationConverterTest extends Specification { private static final String MULTIPLE_ATTRIBUTE_BUCKETS = "md/visualization/multipleAttributeBucketsVisualization.json" private static final String STACKED_COLUMN_CHART = "md/visualization/stackedColumnChart.json" private static final String LINE_CHART = "md/visualization/lineChart.json" + private static final String TABLE_WITH_TOTALS = "md/visualization/complexTableWithTotals.json" + private static final String AFM_FROM_TABLE_WITH_TOTALS = "executeafm/afm/complextTableWithTotalsConvertedAfm.json" + private static final String EXECUTION_FROM_TABLE_WITH_TOTALS = "executeafm/executionComplexTableConverted.json" @SuppressWarnings("GrDeprecatedAPIUsage") def "should convert complex"() { @@ -72,7 +81,7 @@ class VisualizationConverterTest extends Specification { ), "measure1", "Measure 1 alias", null) ], - null + [] ) VisualizationObject visualizationObject = readObjectFromResource("/$COMPLEX_VISUALIZATION", VisualizationObject) Afm converted = convertToAfm(visualizationObject) @@ -83,7 +92,7 @@ class VisualizationConverterTest extends Specification { def "should convert simple"() { given: - Afm expectedAfm = new Afm([], [], [], null) + Afm expectedAfm = new Afm([], [], [], []) VisualizationObject visualizationObject = readObjectFromResource("/$SIMPLE_VISUALIZATION", VisualizationObject) Afm converted = convertToAfm(visualizationObject) @@ -91,6 +100,15 @@ class VisualizationConverterTest extends Specification { that converted, jsonEquals(expectedAfm) } + def "should convert AFM of complex pivot table with totals"() { + given: + Afm expectedAfm = readObjectFromResource("/$AFM_FROM_TABLE_WITH_TOTALS", Afm) + VisualizationObject visualizationObject = readObjectFromResource("/$TABLE_WITH_TOTALS", VisualizationObject) + Afm converted = convertToAfmWithNativeTotals(visualizationObject) + + expect: + that converted, jsonEquals(expectedAfm) + } @Unroll def "should generate result spec for table with default sorting from #name"() { @@ -119,6 +137,58 @@ class VisualizationConverterTest extends Specification { ) } + def "should generate result spec for complex table with totals"() { + when: + VisualizationObject vo = readObjectFromResource("/$TABLE_WITH_TOTALS", VisualizationObject) + Function getter = { vizObject -> + Stub(VisualizationClass) { + getVisualizationType() >> VisualizationType.TABLE + getUri() >> vo.getVisualizationClassUri() + } + } + ResultSpec converted = convertToResultSpecWithTotals(vo, getter) + then: + that converted, jsonEquals( + // pivot table is simplified to a basic table (2 dimensions: measureGroup and attributes) + new ResultSpec([ + new Dimension( + [ + "e4bb25477bca4fb2a29a4b80d94568d4", + "9008f5d33b3e41279402a25e2f05d0c9", + "023641d306f84921be39d0aa1d6464db", + "a22843f5d77f48b4938ccfb460eb8be4", + "a77983fcc9574f6bad6be1d3cb08bf71" + ], + [ + new TotalItem("fd0164f14ec2444b9b5a7140ce059036", "nat", "e4bb25477bca4fb2a29a4b80d94568d4"), + new TotalItem("fd0164f14ec2444b9b5a7140ce059036", "nat", "9008f5d33b3e41279402a25e2f05d0c9"), + new TotalItem("fd0164f14ec2444b9b5a7140ce059036", "nat", "a22843f5d77f48b4938ccfb460eb8be4"), + new TotalItem("fd0164f14ec2444b9b5a7140ce059036", "nat", "a77983fcc9574f6bad6be1d3cb08bf71"), + new TotalItem("fd0164f14ec2444b9b5a7140ce059036", "sum", "e4bb25477bca4fb2a29a4b80d94568d4"), + new TotalItem("fd0164f14ec2444b9b5a7140ce059036", "nat", "023641d306f84921be39d0aa1d6464db") + ] as Set + ), + new Dimension(["measureGroup"], null) + ], [new AttributeSortItem("asc", "e4bb25477bca4fb2a29a4b80d94568d4", null)]) + ) + } + + def "should generate execution for complex table with totals"() { + given: + Execution expectedExecution = readObjectFromResource("/$EXECUTION_FROM_TABLE_WITH_TOTALS", Execution) + VisualizationObject vo = readObjectFromResource("/$TABLE_WITH_TOTALS", VisualizationObject) + Function getter = { vizObject -> + Stub(VisualizationClass) { + getVisualizationType() >> VisualizationType.TABLE + getUri() >> vo.getVisualizationClassUri() + } + } + Execution converted = VisualizationConverter.convertToExecutionWithTotals(vo, getter) + + expect: + that converted, jsonEquals(expectedExecution) + } + @Unroll def "should generate result spec for #type"() { given: diff --git a/gooddata-java-model/src/test/resources/executeafm/afm/complextTableWithTotalsConvertedAfm.json b/gooddata-java-model/src/test/resources/executeafm/afm/complextTableWithTotalsConvertedAfm.json new file mode 100644 index 000000000..ae37d654d --- /dev/null +++ b/gooddata-java-model/src/test/resources/executeafm/afm/complextTableWithTotalsConvertedAfm.json @@ -0,0 +1,85 @@ +{ + "measures": [ + { + "localIdentifier": "fd0164f14ec2444b9b5a7140ce059036", + "definition": { + "measureDefinition": { + "filters": [], + "item": { + "uri": "/gdc/md/w3hub93g7fwmvx60pkt2v8cr56530t0l/obj/9211" + } + } + }, + "alias": "_Close [BOP]" + } + ], + "attributes": [ + { + "displayForm": { + "uri": "/gdc/md/w3hub93g7fwmvx60pkt2v8cr56530t0l/obj/1024" + }, + "localIdentifier": "e4bb25477bca4fb2a29a4b80d94568d4" + }, + { + "displayForm": { + "uri": "/gdc/md/w3hub93g7fwmvx60pkt2v8cr56530t0l/obj/1028" + }, + "localIdentifier": "9008f5d33b3e41279402a25e2f05d0c9" + }, + { + "displayForm": { + "uri": "/gdc/md/w3hub93g7fwmvx60pkt2v8cr56530t0l/obj/1086" + }, + "localIdentifier": "023641d306f84921be39d0aa1d6464db" + }, + { + "displayForm": { + "uri": "/gdc/md/w3hub93g7fwmvx60pkt2v8cr56530t0l/obj/1805" + }, + "localIdentifier": "a22843f5d77f48b4938ccfb460eb8be4" + }, + { + "displayForm": { + "uri": "/gdc/md/w3hub93g7fwmvx60pkt2v8cr56530t0l/obj/1094" + }, + "localIdentifier": "a77983fcc9574f6bad6be1d3cb08bf71" + } + ], + "nativeTotals": [ + { + "measureIdentifier": "fd0164f14ec2444b9b5a7140ce059036", + "attributeIdentifiers": [] + }, + { + "measureIdentifier": "fd0164f14ec2444b9b5a7140ce059036", + "attributeIdentifiers": [ + "e4bb25477bca4fb2a29a4b80d94568d4" + ] + }, + { + "measureIdentifier": "fd0164f14ec2444b9b5a7140ce059036", + "attributeIdentifiers": [ + "e4bb25477bca4fb2a29a4b80d94568d4", + "9008f5d33b3e41279402a25e2f05d0c9" + ] + }, + { + "measureIdentifier": "fd0164f14ec2444b9b5a7140ce059036", + "attributeIdentifiers": [ + "e4bb25477bca4fb2a29a4b80d94568d4", + "9008f5d33b3e41279402a25e2f05d0c9", + "023641d306f84921be39d0aa1d6464db" + ] + }, + { + "measureIdentifier": "fd0164f14ec2444b9b5a7140ce059036", + "attributeIdentifiers": [ + "e4bb25477bca4fb2a29a4b80d94568d4", + "9008f5d33b3e41279402a25e2f05d0c9", + "023641d306f84921be39d0aa1d6464db", + "a22843f5d77f48b4938ccfb460eb8be4" + ] + } + ], + "filters":[] +} diff --git a/gooddata-java-model/src/test/resources/executeafm/executionComplexTableConverted.json b/gooddata-java-model/src/test/resources/executeafm/executionComplexTableConverted.json new file mode 100644 index 000000000..b20f4ed76 --- /dev/null +++ b/gooddata-java-model/src/test/resources/executeafm/executionComplexTableConverted.json @@ -0,0 +1,147 @@ +{ + "execution": { + "afm": { + "attributes": [ + { + "displayForm": { + "uri": "/gdc/md/w3hub93g7fwmvx60pkt2v8cr56530t0l/obj/1024" + }, + "localIdentifier": "e4bb25477bca4fb2a29a4b80d94568d4" + }, + { + "displayForm": { + "uri": "/gdc/md/w3hub93g7fwmvx60pkt2v8cr56530t0l/obj/1028" + }, + "localIdentifier": "9008f5d33b3e41279402a25e2f05d0c9" + }, + { + "displayForm": { + "uri": "/gdc/md/w3hub93g7fwmvx60pkt2v8cr56530t0l/obj/1086" + }, + "localIdentifier": "023641d306f84921be39d0aa1d6464db" + }, + { + "displayForm": { + "uri": "/gdc/md/w3hub93g7fwmvx60pkt2v8cr56530t0l/obj/1805" + }, + "localIdentifier": "a22843f5d77f48b4938ccfb460eb8be4" + }, + { + "displayForm": { + "uri": "/gdc/md/w3hub93g7fwmvx60pkt2v8cr56530t0l/obj/1094" + }, + "localIdentifier": "a77983fcc9574f6bad6be1d3cb08bf71" + } + ], + "filters": [], + "measures": [ + { + "definition": { + "measureDefinition": { + "item": { + "uri": "/gdc/md/w3hub93g7fwmvx60pkt2v8cr56530t0l/obj/9211" + }, + "filters": [] + } + }, + "localIdentifier": "fd0164f14ec2444b9b5a7140ce059036", + "alias": "_Close [BOP]" + } + ], + "nativeTotals": [ + { + "measureIdentifier": "fd0164f14ec2444b9b5a7140ce059036", + "attributeIdentifiers": [] + }, + { + "measureIdentifier": "fd0164f14ec2444b9b5a7140ce059036", + "attributeIdentifiers": [ + "e4bb25477bca4fb2a29a4b80d94568d4" + ] + }, + { + "measureIdentifier": "fd0164f14ec2444b9b5a7140ce059036", + "attributeIdentifiers": [ + "e4bb25477bca4fb2a29a4b80d94568d4", + "9008f5d33b3e41279402a25e2f05d0c9" + ] + }, + { + "measureIdentifier": "fd0164f14ec2444b9b5a7140ce059036", + "attributeIdentifiers": [ + "e4bb25477bca4fb2a29a4b80d94568d4", + "9008f5d33b3e41279402a25e2f05d0c9", + "023641d306f84921be39d0aa1d6464db" + ] + }, + { + "measureIdentifier": "fd0164f14ec2444b9b5a7140ce059036", + "attributeIdentifiers": [ + "e4bb25477bca4fb2a29a4b80d94568d4", + "9008f5d33b3e41279402a25e2f05d0c9", + "023641d306f84921be39d0aa1d6464db", + "a22843f5d77f48b4938ccfb460eb8be4" + ] + } + ] + }, + "resultSpec": { + "dimensions": [ + { + "itemIdentifiers": [ + "e4bb25477bca4fb2a29a4b80d94568d4", + "9008f5d33b3e41279402a25e2f05d0c9", + "023641d306f84921be39d0aa1d6464db", + "a22843f5d77f48b4938ccfb460eb8be4", + "a77983fcc9574f6bad6be1d3cb08bf71" + ], + "totals": [ + { + "measureIdentifier": "fd0164f14ec2444b9b5a7140ce059036", + "type": "nat", + "attributeIdentifier": "e4bb25477bca4fb2a29a4b80d94568d4" + }, + { + "measureIdentifier": "fd0164f14ec2444b9b5a7140ce059036", + "type": "nat", + "attributeIdentifier": "9008f5d33b3e41279402a25e2f05d0c9" + }, + { + "measureIdentifier": "fd0164f14ec2444b9b5a7140ce059036", + "type": "nat", + "attributeIdentifier": "a22843f5d77f48b4938ccfb460eb8be4" + }, + { + "measureIdentifier": "fd0164f14ec2444b9b5a7140ce059036", + "type": "nat", + "attributeIdentifier": "a77983fcc9574f6bad6be1d3cb08bf71" + }, + { + "measureIdentifier": "fd0164f14ec2444b9b5a7140ce059036", + "type": "sum", + "attributeIdentifier": "e4bb25477bca4fb2a29a4b80d94568d4" + }, + { + "measureIdentifier": "fd0164f14ec2444b9b5a7140ce059036", + "type": "nat", + "attributeIdentifier": "023641d306f84921be39d0aa1d6464db" + } + ] + }, + { + "itemIdentifiers": [ + "measureGroup" + ] + } + ], + "sorts": [ + { + "attributeSortItem": { + "direction": "asc", + "attributeIdentifier": "e4bb25477bca4fb2a29a4b80d94568d4" + } + } + ] + } + } +} diff --git a/gooddata-java-model/src/test/resources/md/visualization/complexTableWithTotals.json b/gooddata-java-model/src/test/resources/md/visualization/complexTableWithTotals.json new file mode 100644 index 000000000..9dc185f47 --- /dev/null +++ b/gooddata-java-model/src/test/resources/md/visualization/complexTableWithTotals.json @@ -0,0 +1,117 @@ +{ + "visualizationObject": { + "content": { + "buckets": [ + { + "localIdentifier": "measures", + "items": [ + { + "measure": { + "definition": { + "measureDefinition": { + "item": { + "uri": "/gdc/md/w3hub93g7fwmvx60pkt2v8cr56530t0l/obj/9211" + } + } + }, + "title": "_Close [BOP]", + "localIdentifier": "fd0164f14ec2444b9b5a7140ce059036" + } + } + ] + }, + { + "items": [ + { + "visualizationAttribute": { + "displayForm": { + "uri": "/gdc/md/w3hub93g7fwmvx60pkt2v8cr56530t0l/obj/1024" + }, + "localIdentifier": "e4bb25477bca4fb2a29a4b80d94568d4" + } + }, + { + "visualizationAttribute": { + "displayForm": { + "uri": "/gdc/md/w3hub93g7fwmvx60pkt2v8cr56530t0l/obj/1028" + }, + "localIdentifier": "9008f5d33b3e41279402a25e2f05d0c9" + } + }, + { + "visualizationAttribute": { + "displayForm": { + "uri": "/gdc/md/w3hub93g7fwmvx60pkt2v8cr56530t0l/obj/1086" + }, + "localIdentifier": "023641d306f84921be39d0aa1d6464db" + } + } + ], + "totals": [ + { + "measureIdentifier": "fd0164f14ec2444b9b5a7140ce059036", + "attributeIdentifier": "e4bb25477bca4fb2a29a4b80d94568d4", + "type": "sum" + }, + { + "measureIdentifier": "fd0164f14ec2444b9b5a7140ce059036", + "attributeIdentifier": "e4bb25477bca4fb2a29a4b80d94568d4", + "type": "nat" + }, + { + "measureIdentifier": "fd0164f14ec2444b9b5a7140ce059036", + "attributeIdentifier": "9008f5d33b3e41279402a25e2f05d0c9", + "type": "nat" + }, + { + "attributeIdentifier": "023641d306f84921be39d0aa1d6464db", + "measureIdentifier": "fd0164f14ec2444b9b5a7140ce059036", + "type": "nat" + } + ], + "localIdentifier": "attribute" + }, + { + "items": [ + { + "visualizationAttribute": { + "localIdentifier": "a22843f5d77f48b4938ccfb460eb8be4", + "displayForm": { + "uri": "/gdc/md/w3hub93g7fwmvx60pkt2v8cr56530t0l/obj/1805" + } + } + }, + { + "visualizationAttribute": { + "localIdentifier": "a77983fcc9574f6bad6be1d3cb08bf71", + "displayForm": { + "uri": "/gdc/md/w3hub93g7fwmvx60pkt2v8cr56530t0l/obj/1094" + } + } + } + ], + "totals": [ + { + "type": "nat", + "attributeIdentifier": "a22843f5d77f48b4938ccfb460eb8be4", + "measureIdentifier": "fd0164f14ec2444b9b5a7140ce059036" + }, + { + "type": "nat", + "attributeIdentifier": "a77983fcc9574f6bad6be1d3cb08bf71", + "measureIdentifier": "fd0164f14ec2444b9b5a7140ce059036" + } + ], + "localIdentifier": "columns" + } + ], + "properties": "{\"sortItems\":[{\"attributeSortItem\":{\"attributeIdentifier\":\"e4bb25477bca4fb2a29a4b80d94568d4\",\"direction\":\"asc\"}}]}", + "visualizationClass": { + "uri": "/gdc/md/w3hub93g7fwmvx60pkt2v8cr56530t0l/obj/75547" + } + }, + "meta": { + "title": "complex-with-totals" + } + } +} diff --git a/gooddata-java-model/src/test/resources/md/visualization/mixedBucketWithTotals.json b/gooddata-java-model/src/test/resources/md/visualization/mixedBucketWithTotals.json new file mode 100644 index 000000000..988a10a80 --- /dev/null +++ b/gooddata-java-model/src/test/resources/md/visualization/mixedBucketWithTotals.json @@ -0,0 +1,37 @@ +{ + "localIdentifier": "attributeBucket", + "items": [ + { + "visualizationAttribute": { + "localIdentifier": "attribute", + "displayForm": { + "uri": "/uri/to/displayForm" + }, + "alias": "Attribute Alias" + } + }, { + "measure": { + "localIdentifier": "measure", + "title": "Measure", + "alias": "Measure Alias", + "definition": { + "measureDefinition": { + "item": { + "uri": "/uri/to/measure" + }, + "aggregation": "sum", + "computeRatio": false, + "filters": [] + } + } + } + } + ], + "totals": [ + { + "measureIdentifier": "measure", + "type": "nat", + "attributeIdentifier": "attribute" + } + ] +}