package org.sigmah.server.report.model.generator;
/*
* #%L
* Sigmah
* %%
* Copyright (C) 2010 - 2016 URD
* %%
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public
* License along with this program. If not, see
* <http://www.gnu.org/licenses/gpl-3.0.html>.
* #L%
*/
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
import org.sigmah.server.dao.PivotDAO;
import org.sigmah.server.dao.PivotDAO.Bucket;
import org.sigmah.server.util.CalendarDates;
import org.sigmah.shared.dto.pivot.content.DimensionCategory;
import org.sigmah.shared.dto.pivot.content.LabeledDimensionCategory;
import org.sigmah.shared.dto.pivot.content.MonthCategory;
import org.sigmah.shared.dto.pivot.content.PivotTableData;
import org.sigmah.shared.dto.pivot.content.QuarterCategory;
import org.sigmah.shared.dto.pivot.content.SimpleCategory;
import org.sigmah.shared.dto.pivot.content.YearCategory;
import org.sigmah.shared.dto.pivot.model.DateDimension;
import org.sigmah.shared.dto.pivot.model.DateUnit;
import org.sigmah.shared.dto.pivot.model.Dimension;
import org.sigmah.shared.dto.pivot.model.PivotElement;
import org.sigmah.shared.dto.referential.DimensionType;
import org.sigmah.shared.util.DateRange;
import org.sigmah.shared.util.Dates;
import org.sigmah.shared.util.Filter;
import org.sigmah.shared.util.Month;
/**
* @author Alex Bertram (akbertram@gmail.com)
* @param <T>
*/
public abstract class PivotGenerator<T extends PivotElement> extends BaseGenerator<T> {
private final Dates dates = new CalendarDates();
protected PivotTableData generateData(int userId, Locale locale,
T element,
Filter filter,
List<Dimension> rowDims, List<Dimension> colDims) {
Populator populator = new Populator(element, rowDims, colDims, locale);
if(element.isShowEmptyCells()) {
Set<Dimension> dateDimensions = new HashSet<Dimension>();
Set<Dimension> otherDimensions = new HashSet<Dimension>();
for(Dimension dim : (Set<Dimension>)element.allDimensions()) {
if(dim instanceof DateDimension) {
dateDimensions.add(dim);
} else {
otherDimensions.add(dim);
}
}
List<Bucket> buckets = Collections.emptyList();
if(!otherDimensions.isEmpty()) {
buckets = pivotDAO.queryDimensionCategories(userId, filter, otherDimensions) ;
}
if(!filter.getDateRange().isClosed() && !dateDimensions.isEmpty()) {
throw new RuntimeException("If a date dimension is specified in rows/cols and showEmptyCells is set, " +
"than a closed DateRange filter must be provided");
}
populator.addHeaders(buckets,filter.getDateRange());
}
populator.addValues(pivotDAO.aggregate(
userId, filter,
element.allDimensions()));
if(!rowDims.contains(new Dimension(DimensionType.Indicator))) {
populator.getTable().getRootRow().total();
}
return populator.getTable();
}
private class Populator {
private Map<Dimension, Comparator<PivotTableData.Axis>> comparators;
private List<Dimension> rowDims;
private List<Dimension> colDims;
private PivotTableData table;
private Locale locale;
public Populator(T element, List<Dimension> rowDims, List<Dimension> colDims, Locale locale) {
this.rowDims = rowDims;
this.colDims = colDims;
this.locale = locale;
comparators = createComparators(element.allDimensions());
table = new PivotTableData(rowDims, colDims);
}
private void addValues(List<Bucket> buckets) {
for(Bucket bucket : buckets) {
PivotTableData.Axis row = findRowNode(bucket);
row.setValue(findColumnNode(bucket), bucket.doubleValue(), bucket.count(), bucket.aggregation());
}
}
public PivotTableData getTable() {
return table;
}
private PivotTableData.Axis findColumnNode(Bucket bucket) {
return colDims.isEmpty() ? table.getRootColumn() :
find(table.getRootColumn(), colDims.iterator(), bucket);
}
private PivotTableData.Axis findRowNode(Bucket bucket) {
return rowDims.isEmpty() ? table.getRootRow() :
find(table.getRootRow(), rowDims.iterator(), bucket);
}
/**
* Recursively descends the pivot table axis to find or add the leaf node
* for this bucket.
*
* @param axis
* @param dimensionIterator
* @param bucket
* @return
*/
private PivotTableData.Axis find(PivotTableData.Axis axis,
Iterator<Dimension> dimensionIterator,
PivotDAO.Bucket bucket) {
Dimension childDimension = dimensionIterator.next();
DimensionCategory category = bucket.getCategory(childDimension);
PivotTableData.Axis child = null;
if(category == null) {
// skip this dimension, it's value is missing
child = axis;
} else {
child = axis.getChild(category);
if (child == null) {
String categoryLabel = childDimension.getLabel(category);
if (categoryLabel == null) {
categoryLabel = renderLabel(childDimension, category);
}
child = axis.addChild(childDimension,
category,
categoryLabel,
comparators.get(childDimension));
}
}
if (dimensionIterator.hasNext()) {
return find(child, dimensionIterator, bucket);
} else {
return child;
}
}
private String renderLabel(Dimension childDimension, DimensionCategory category) {
if (category instanceof LabeledDimensionCategory) {
return ((LabeledDimensionCategory) category).getLabel();
} else if (category instanceof YearCategory) {
return Integer.toString(((YearCategory) category).getYear());
} else if (category instanceof QuarterCategory) {
// TODO: i18n
QuarterCategory quarter = (QuarterCategory) category;
return Integer.toString(quarter.getYear()) + "T" + quarter.getQuarter();
} else if (category instanceof MonthCategory) {
return renderMonthLabel(category);
} else if (category instanceof SimpleCategory) {
return ((SimpleCategory) category).getLabel();
}
return "(Totale)"; // TODO
}
private String renderMonthLabel(DimensionCategory category) {
SimpleDateFormat format = (SimpleDateFormat) SimpleDateFormat.getDateInstance(SimpleDateFormat.SHORT, locale);
String[] months = format.getDateFormatSymbols().getShortMonths();
return months[((MonthCategory) category).getMonth() - 1];
}
private Dimension next(List<Dimension> list, Dimension parent) {
int index = list.indexOf(parent);
return index+1 < list.size() ? list.get(index+1) : null;
}
private boolean hasNext(List<Dimension> list, Dimension parent) {
int index = list.indexOf(parent);
return index+1 < list.size();
}
private void addHeaders(List<Bucket> databaseRows, DateRange dateRange) {
if(!rowDims.isEmpty()) {
addHeaders(table.getRootRow(), rowDims, databaseRows, dateRange);
}
if(!colDims.isEmpty()) {
addHeaders(table.getRootColumn(), colDims, databaseRows, dateRange);
}
}
private void addHeaders(PivotTableData.Axis parent, List<Dimension> dims, List<Bucket> buckets, DateRange range) {
addHeaders(parent, dims, next(dims, parent.getDimension()), buckets, range);
}
private void addHeaders(PivotTableData.Axis parent, List<Dimension> dims, Dimension childDimension, List<Bucket> buckets, DateRange range) {
if(childDimension instanceof DateDimension) {
if(((DateDimension) childDimension).getUnit() == DateUnit.YEAR) {
addYears(parent, dims, buckets, range);
} else if(((DateDimension) childDimension).getUnit() == DateUnit.MONTH) {
addMonths(parent, dims, buckets, range);
}
} else if(childDimension != null) {
// first add those who are missing a value for this dimension
addHeaders(parent, dims, next(dims, childDimension), filter(buckets, childDimension, null), range);
for(DimensionCategory category : categories(buckets, childDimension)) {
PivotTableData.Axis child = parent.addChild(childDimension, category, renderLabel(childDimension, category), comparators.get(childDimension));
addHeaders(child, dims, filter(buckets, childDimension, category), range);
}
}
}
private Set<DimensionCategory> categories(List<Bucket> buckets, Dimension dim) {
Set<DimensionCategory> set = new HashSet<DimensionCategory>();
for(Bucket bucket : buckets) {
DimensionCategory category = bucket.getCategory(dim);
if(category != null) {
set.add(category);
}
}
return set;
}
private List<Bucket> filter(List<Bucket> buckets, Dimension dim, DimensionCategory cat) {
List<Bucket> filtered = new ArrayList<Bucket>();
for(Bucket bucket : buckets) {
DimensionCategory bucketCategory = bucket.getCategory(dim);
if( (cat == null && bucketCategory == null) ||
(cat != null && cat.equals(bucketCategory))) {
filtered.add(bucket);
}
}
return filtered;
}
private void addYears(PivotTableData.Axis parent, List<Dimension> dims, List<Bucket> buckets, DateRange dateRange) {
DateDimension yearDim = new DateDimension(DateUnit.YEAR);
int startYear = dates.getYear(dateRange.getMinDate());
int endYear = dates.getYear(dateRange.getMaxDate());
for(int year = startYear; year<=endYear;++year) {
PivotTableData.Axis child = parent.addChild(yearDim, new YearCategory(year), Integer.toString(year), DEFAULT_COMPARATOR);
addHeaders(child, dims, buckets, DateRange.intersection(dateRange, dates.yearRange(year)));
}
}
private void addMonths(PivotTableData.Axis parent, List<Dimension> dims, List<Bucket> buckets, DateRange dateRange) {
DateDimension monthDim = new DateDimension(DateUnit.MONTH);
Month startMonth = new Month(dates.getYear(dateRange.getMinDate()), dates.getMonth(dateRange.getMinDate()));
Month endMonth = new Month(dates.getYear(dateRange.getMaxDate()), dates.getMonth(dateRange.getMaxDate()));
for(Month m=startMonth;m.compareTo(endMonth)<=0;m=m.next()) {
MonthCategory monthCategory = new MonthCategory(m.getYear(), m.getMonth());
PivotTableData.Axis child = parent.addChild(monthDim, monthCategory, renderMonthLabel(monthCategory), DEFAULT_COMPARATOR );
addHeaders(child, dims, buckets, DateRange.intersection(dateRange, dates.monthRange(m)));
}
}
}
protected Map<Dimension, Comparator<PivotTableData.Axis>> createComparators(Set<Dimension> dimensions) {
Map<Dimension, Comparator<PivotTableData.Axis>> map =
new HashMap<Dimension, Comparator<PivotTableData.Axis>>();
for (Dimension dimension : dimensions) {
if (dimension.isOrderDefined()) {
map.put(dimension, new DefinedCategoryComparator(dimension.getOrdering()));
} else {
map.put(dimension, new CategoryComparator());
}
}
return map;
}
private static final CategoryComparator DEFAULT_COMPARATOR = new CategoryComparator();
private static class CategoryComparator implements Comparator<PivotTableData.Axis> {
@Override
public int compare(PivotTableData.Axis a1, PivotTableData.Axis a2) {
Comparable c1 = a1.getCategory().getSortKey();
Comparable c2 = a2.getCategory().getSortKey();
if (c1 == null && c2 == null) {
return 0;
}
if (c1 == null) {
return -1;
}
if (c2 == null) {
return 1;
}
if(c1.getClass() != c2.getClass()) {
// this occurs if we have an unbalanced tree and we end up comparing categories of
// different dimensions to each other. we sort by class name to get a stable, if arbitrary
// ordering
return c1.getClass().getSimpleName().compareTo(c2.getClass().getSimpleName());
}
return c1.compareTo(c2);
}
}
private static class DefinedCategoryComparator implements Comparator<PivotTableData.Axis> {
private final Map<DimensionCategory, Integer> orderMap;
public DefinedCategoryComparator(List<DimensionCategory> order) {
orderMap = new HashMap<DimensionCategory, Integer>();
for (int i = 0; i != order.size(); ++i) {
orderMap.put(order.get(i), i);
}
}
@Override
public int compare(PivotTableData.Axis a1, PivotTableData.Axis a2) {
Integer o1 = orderMap.get(a1.getCategory());
Integer o2 = orderMap.get(a2.getCategory());
if (o1 == null) {
o1 = Integer.MAX_VALUE;
}
if (o2 == null) {
o2 = Integer.MAX_VALUE;
}
return o1.compareTo(o2);
}
}
}