Skip to content

Commit 9c5f1d0

Browse files
Merge pull request #1209 from gooddata/afmx/rollup-totals
Introduce totals support in visualizations and their conversions
2 parents 7080734 + caa39b0 commit 9c5f1d0

File tree

9 files changed

+730
-26
lines changed

9 files changed

+730
-26
lines changed

gooddata-java-model/src/main/java/com/gooddata/sdk/model/md/visualization/Bucket.java

Lines changed: 35 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,10 @@
1111
import com.fasterxml.jackson.annotation.JsonInclude;
1212
import com.fasterxml.jackson.annotation.JsonProperty;
1313
import com.gooddata.sdk.model.executeafm.afm.LocallyIdentifiable;
14+
import com.gooddata.sdk.model.executeafm.resultspec.TotalItem;
1415

1516
import java.io.Serializable;
17+
import java.util.ArrayList;
1618
import java.util.List;
1719
import java.util.Objects;
1820

@@ -26,17 +28,33 @@ public class Bucket implements Serializable, LocallyIdentifiable {
2628
private static final long serialVersionUID = -7718720886547680021L;
2729
private final String localIdentifier;
2830
private final List<BucketItem> items;
31+
private final List<TotalItem> totals;
32+
33+
/**
34+
* Creates new instance of bucket without totals
35+
*
36+
* @param localIdentifier local identifier of bucket
37+
* @param items list of {@link BucketItem}s for this bucket
38+
*/
39+
public Bucket(@JsonProperty("localIdentifier") final String localIdentifier,
40+
@JsonProperty("items") final List<BucketItem> items) {
41+
this(localIdentifier, items, null);
42+
}
2943

3044
/**
3145
* Creates new instance of bucket
46+
*
3247
* @param localIdentifier local identifier of bucket
33-
* @param items list of {@link BucketItem}s for this bucket
48+
* @param items list of {@link BucketItem}s for this bucket
49+
* @param totals list of {@link TotalItem}s for this bucket
3450
*/
3551
@JsonCreator
3652
public Bucket(@JsonProperty("localIdentifier") final String localIdentifier,
37-
@JsonProperty("items") final List<BucketItem> items) {
53+
@JsonProperty("items") final List<BucketItem> items,
54+
@JsonProperty("totals") List<TotalItem> totals) {
3855
this.localIdentifier = localIdentifier;
3956
this.items = items;
57+
this.totals = totals;
4058
}
4159

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

74+
/**
75+
* @return list of defined {@link TotalItem}s
76+
*/
77+
public List<TotalItem> getTotals() {
78+
return totals;
79+
}
80+
5681
@JsonIgnore
5782
VisualizationAttribute getOnlyAttribute() {
5883
if (getItems() != null && getItems().size() == 1) {
@@ -67,15 +92,18 @@ VisualizationAttribute getOnlyAttribute() {
6792

6893
@Override
6994
public boolean equals(Object o) {
70-
if (this == o) return true;
71-
if (o == null || getClass() != o.getClass()) return false;
95+
if (this == o)
96+
return true;
97+
if (o == null || getClass() != o.getClass())
98+
return false;
7299
Bucket bucket = (Bucket) o;
73-
return Objects.equals(localIdentifier, bucket.localIdentifier) &&
74-
Objects.equals(items, bucket.items);
100+
return Objects.equals(localIdentifier, bucket.localIdentifier)
101+
&& Objects.equals(items, bucket.items)
102+
&& Objects.equals(totals, bucket.totals);
75103
}
76104

77105
@Override
78106
public int hashCode() {
79-
return Objects.hash(localIdentifier, items);
107+
return Objects.hash(localIdentifier, items, totals);
80108
}
81109
}

gooddata-java-model/src/main/java/com/gooddata/sdk/model/md/visualization/VisualizationConverter.java

Lines changed: 161 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
import com.gooddata.sdk.model.executeafm.Execution;
1212
import com.gooddata.sdk.model.executeafm.afm.Afm;
1313
import com.gooddata.sdk.model.executeafm.afm.AttributeItem;
14+
import com.gooddata.sdk.model.executeafm.afm.NativeTotalItem;
1415
import com.gooddata.sdk.model.executeafm.afm.filter.CompatibilityFilter;
1516
import com.gooddata.sdk.model.executeafm.afm.filter.DateFilter;
1617
import com.gooddata.sdk.model.executeafm.afm.filter.ExtendedFilter;
@@ -24,8 +25,11 @@
2425
import com.gooddata.sdk.model.executeafm.resultspec.Dimension;
2526
import com.gooddata.sdk.model.executeafm.resultspec.ResultSpec;
2627
import com.gooddata.sdk.model.executeafm.resultspec.SortItem;
28+
import com.gooddata.sdk.model.executeafm.resultspec.TotalItem;
29+
import com.gooddata.sdk.model.md.report.Total;
2730

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

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

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

87+
/**
88+
* Generate Execution from Visualization object with totals included.
89+
*
90+
* @param visualizationObject which will be converted to {@link Execution}
91+
* @param visualizationClassGetter {@link Function} for fetching VisualizationClass,
92+
* which is necessary for correct generation of {@link ResultSpec}
93+
* @return {@link Execution} object
94+
* @see #convertToExecutionWithTotals(VisualizationObject, VisualizationClass)
95+
*/
96+
public static Execution convertToExecutionWithTotals(final VisualizationObject visualizationObject,
97+
final Function<String, VisualizationClass> visualizationClassGetter) {
98+
notNull(visualizationObject, "visualizationObject");
99+
notNull(visualizationClassGetter, "visualizationClassGetter");
100+
return convertToExecutionWithTotals(visualizationObject,
101+
visualizationClassGetter.apply(visualizationObject.getVisualizationClassUri()));
102+
}
103+
104+
/**
105+
* Generate Execution from Visualization object with totals included.
106+
*
107+
* @param visualizationObject which will be converted to {@link Execution}
108+
* @param visualizationClass visualizationClass, which is necessary for correct generation of {@link ResultSpec}
109+
* @return {@link Execution} object
110+
* @see #convertToAfmWithNativeTotals(VisualizationObject)
111+
* @see #convertToResultSpecWithTotals(VisualizationObject, VisualizationClass)
112+
*/
113+
public static Execution convertToExecutionWithTotals(final VisualizationObject visualizationObject,
114+
final VisualizationClass visualizationClass) {
115+
notNull(visualizationObject, "visualizationObject");
116+
notNull(visualizationClass, "visualizationClass");
117+
ResultSpec resultSpec = convertToResultSpecWithTotals(visualizationObject, visualizationClass);
118+
Afm afm = convertToAfmWithNativeTotals(visualizationObject);
119+
return new Execution(afm, resultSpec);
120+
}
121+
78122
/**
79123
* Generate Afm from Visualization object.
124+
* <p>
125+
* <b>NOTE: native totals are not included in this conversion</b>
80126
*
81127
* @param visualizationObject which will be converted to {@link Execution}
82128
* @return {@link Afm} object
83129
*/
84130
public static Afm convertToAfm(final VisualizationObject visualizationObject) {
131+
notNull(visualizationObject, "visualizationObject");
132+
final VisualizationObject visualizationObjectWithoutTotals = removeTotals(visualizationObject);
133+
return convertToAfmWithNativeTotals(visualizationObjectWithoutTotals);
134+
}
135+
136+
/**
137+
* Generate Afm from Visualization object with native totals included.
138+
*
139+
* @param visualizationObject which will be converted to {@link Execution}
140+
* @return {@link Afm} object
141+
*/
142+
public static Afm convertToAfmWithNativeTotals(final VisualizationObject visualizationObject) {
85143
notNull(visualizationObject, "visualizationObject");
86144
final List<AttributeItem> attributes = convertAttributes(visualizationObject.getAttributes());
87145
final List<CompatibilityFilter> filters = convertFilters(visualizationObject.getFilters());
88146
final List<MeasureItem> measures = convertMeasures(visualizationObject.getMeasures());
147+
final List<NativeTotalItem> totals = convertNativeTotals(visualizationObject);
89148

90-
return new Afm(attributes, filters, measures, null);
149+
return new Afm(attributes, filters, measures, totals);
91150
}
92151

93152
/**
94153
* Generate ResultSpec from Visualization object. Currently {@link ResultSpec}'s {@link Dimension}s can be generated
95154
* for table and four types of chart: bar, column, line and pie.
155+
* <p>
156+
* <b>NOTE: totals are not included in this conversion</b>
96157
*
97158
* @param visualizationObject which will be converted to {@link Execution}
98-
* @param visualizationClassGetter {@link Function} for fetching VisualizationClass, which is necessary for correct generation of {@link ResultSpec}
159+
* @param visualizationClassGetter {@link Function} for fetching VisualizationClass,
160+
* which is necessary for correct generation of {@link ResultSpec}
99161
* @return {@link Execution} object
100162
*/
101163
public static ResultSpec convertToResultSpec(final VisualizationObject visualizationObject,
@@ -109,6 +171,8 @@ public static ResultSpec convertToResultSpec(final VisualizationObject visualiza
109171
/**
110172
* Generate ResultSpec from Visualization object. Currently {@link ResultSpec}'s {@link Dimension}s can be generated
111173
* for table and four types of chart: bar, column, line and pie.
174+
* <p>
175+
* <b>NOTE: totals are not included in this conversion</b>
112176
*
113177
* @param visualizationObject which will be converted to {@link Execution}
114178
* @param visualizationClass VisualizationClass, which is necessary for correct generation of {@link ResultSpec}
@@ -118,6 +182,39 @@ public static ResultSpec convertToResultSpec(final VisualizationObject visualiza
118182
final VisualizationClass visualizationClass) {
119183
notNull(visualizationObject, "visualizationObject");
120184
notNull(visualizationClass, "visualizationClass");
185+
final VisualizationObject visualizationObjectWithoutTotals = removeTotals(visualizationObject);
186+
return convertToResultSpecWithTotals(visualizationObjectWithoutTotals, visualizationClass);
187+
}
188+
189+
/**
190+
* Generate ResultSpec from Visualization object with totals included. Currently {@link ResultSpec}'s {@link Dimension}s
191+
* can be generated for table and four types of chart: bar, column, line and pie.
192+
*
193+
* @param visualizationObject which will be converted to {@link Execution}
194+
* @param visualizationClassGetter {@link Function} for fetching VisualizationClass,
195+
* which is necessary for correct generation of {@link ResultSpec}
196+
* @return {@link Execution} object
197+
*/
198+
public static ResultSpec convertToResultSpecWithTotals(final VisualizationObject visualizationObject,
199+
final Function<String, VisualizationClass> visualizationClassGetter) {
200+
notNull(visualizationObject, "visualizationObject");
201+
notNull(visualizationClassGetter, "visualizationClassGetter");
202+
return convertToResultSpecWithTotals(visualizationObject,
203+
visualizationClassGetter.apply(visualizationObject.getVisualizationClassUri()));
204+
}
205+
206+
/**
207+
* Generate ResultSpec from Visualization object with totals included. Currently {@link ResultSpec}'s {@link Dimension}s
208+
* can be generated for table and four types of chart: bar, column, line and pie.
209+
*
210+
* @param visualizationObject which will be converted to {@link Execution}
211+
* @param visualizationClass VisualizationClass, which is necessary for correct generation of {@link ResultSpec}
212+
* @return {@link Execution} object
213+
*/
214+
public static ResultSpec convertToResultSpecWithTotals(final VisualizationObject visualizationObject,
215+
final VisualizationClass visualizationClass) {
216+
notNull(visualizationObject, "visualizationObject");
217+
notNull(visualizationClass, "visualizationClass");
121218
isTrue(visualizationObject.getVisualizationClassUri().equals(visualizationClass.getUri()),
122219
"visualizationClass URI does not match the URI within visualizationObject, "
123220
+ "you're trying to create ResultSpec for incompatible objects");
@@ -144,6 +241,21 @@ static List<SortItem> parseSorting(final String properties) throws Exception {
144241
return MAPPER.convertValue(nodeSortItems, mapType);
145242
}
146243

244+
/**
245+
* Creates a new {@link VisualizationObject} derived from the original one, with all "totals" removed from its buckets.
246+
* This is to ensure backward compatibility in cases where totals were not previously handled.
247+
*
248+
* @param visualizationObject original {@link VisualizationObject}
249+
* @return a new VisualizationObject derived from the original but without any totals in the buckets.
250+
*/
251+
private static VisualizationObject removeTotals(final VisualizationObject visualizationObject) {
252+
final List<Bucket> bucketsWithoutTotals = visualizationObject.getBuckets().stream()
253+
// create buckets without totals
254+
.map(bucket -> new Bucket(bucket.getLocalIdentifier(), bucket.getItems()))
255+
.collect(toList());
256+
return visualizationObject.withBuckets(bucketsWithoutTotals);
257+
}
258+
147259
private static List<Dimension> getDimensions(final VisualizationObject visualizationObject,
148260
final VisualizationType visualizationType) {
149261
switch (visualizationType) {
@@ -216,12 +328,16 @@ private static List<Dimension> getDimensionsForTable(final VisualizationObject v
216328
List<Dimension> dimensions = new ArrayList<>();
217329

218330
List<VisualizationAttribute> attributes = visualizationObject.getAttributes();
331+
List<TotalItem> totals = visualizationObject.getTotals();
219332

220333
if (!attributes.isEmpty()) {
221-
dimensions.add(new Dimension(attributes.stream()
334+
final Dimension attributeDimension = new Dimension(attributes.stream()
222335
.map(VisualizationAttribute::getLocalIdentifier)
223-
.collect(toList())
224-
));
336+
.collect(toList()));
337+
if (!totals.isEmpty()) {
338+
attributeDimension.setTotals(new HashSet<>(totals));
339+
}
340+
dimensions.add(attributeDimension);
225341
} else {
226342
dimensions.add(new Dimension(new ArrayList<>()));
227343
}
@@ -316,4 +432,43 @@ private static <T> List<T> removeIrrelevantFilters(final List<T> filters) {
316432
})
317433
.collect(Collectors.toList());
318434
}
435+
436+
private static List<NativeTotalItem> convertNativeTotals(final VisualizationObject visualizationObject) {
437+
final List<Bucket> attributeBuckets = getAttributeBuckets(visualizationObject);
438+
final List<String> attributeIds = getIdsFromAttributeBuckets(attributeBuckets);
439+
return attributeBuckets.stream()
440+
.filter(bucket -> bucket.getTotals() != null)
441+
.flatMap(bucket -> bucket.getTotals().stream())
442+
.filter(totalItem -> isNativeTotal(totalItem) && attributeIds.contains(totalItem.getAttributeIdentifier()))
443+
.map(totalItem -> convertToNativeTotalItem(totalItem, attributeIds))
444+
.collect(toList());
445+
}
446+
447+
private static NativeTotalItem convertToNativeTotalItem(TotalItem totalItem, List<String> attributeIds) {
448+
final int attributeIdx = attributeIds.indexOf(totalItem.getAttributeIdentifier());
449+
return new NativeTotalItem(
450+
totalItem.getMeasureIdentifier(),
451+
new ArrayList<>(attributeIds.subList(0, attributeIdx))
452+
);
453+
}
454+
455+
private static List<Bucket> getAttributeBuckets(final VisualizationObject visualizationObject) {
456+
return visualizationObject.getBuckets().stream()
457+
.filter(bucket -> bucket.getItems().stream().allMatch(AttributeItem.class::isInstance))
458+
.collect(toList());
459+
}
460+
461+
private static List<String> getIdsFromAttributeBuckets(final List<Bucket> attributeBuckets) {
462+
return attributeBuckets.stream()
463+
.flatMap(bucket ->
464+
bucket.getItems().stream()
465+
.map(AttributeItem.class::cast)
466+
.map(AttributeItem::getLocalIdentifier)
467+
)
468+
.collect(toList());
469+
}
470+
471+
private static boolean isNativeTotal(TotalItem totalItem) {
472+
return totalItem.getType() != null && Total.NAT.name().equals(totalItem.getType().toUpperCase());
473+
}
319474
}

0 commit comments

Comments
 (0)