/*
* Copyright 2012 Decebal Suiu
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this work except in compliance with
* the License. You may obtain a copy of the License in the LICENSE file, or at:
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on
* an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the
* specific language governing permissions and limitations under the License.
*/
package ro.fortsoft.wicket.pivot;
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.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import org.apache.commons.collections.CollectionUtils;
import org.apache.commons.collections.map.MultiKeyMap;
import ro.fortsoft.wicket.pivot.FieldCalculation.FieldValueProvider;
import ro.fortsoft.wicket.pivot.tree.Node;
import ro.fortsoft.wicket.pivot.tree.Tree;
import ro.fortsoft.wicket.pivot.tree.TreeHelper;
/**
* @author Decebal Suiu
*/
public class DefaultPivotModel implements PivotModel {
private static final long serialVersionUID = 1L;
private PivotDataSource dataSource;
private List<PivotField> fields;
private Tree columnsHeaderTree;
private Tree rowsHeaderTree;
private List<MultiKeyMap> calculatedData; // or use a MultiValueMap from apache commons
private boolean showGrandTotalForColumn;
private boolean showGrandTotalForRow;
private boolean autoCalculate;
public DefaultPivotModel(PivotDataSource dataSource) {
this.dataSource = dataSource;
// init fields
int count = dataSource.getFieldCount();
fields = new ArrayList<PivotField>(count);
for (int i = 0; i < count; i++) {
PivotField field = new PivotField(dataSource.getFieldName(i), i);
field.setTitle(field.getName());
field.setArea(PivotField.Area.UNUSED);
field.setType(dataSource.getFieldType(i));
fields.add(field);
}
}
@Override
public List<PivotField> getFields() {
return fields;
}
@Override
public PivotField getField(String name) {
for (PivotField field : fields) {
if (field.getName().equals(name)) {
return field;
}
}
return null;
}
@Override
public PivotField getField(int index) {
for (PivotField field : fields) {
if (field.getIndex() == index) {
return field;
}
}
return null;
}
@Override
public List<PivotField> getFields(PivotField.Area area) {
List<PivotField> areaFields = new ArrayList<PivotField>();
List<PivotField> fields = getFields();
for (PivotField field : fields) {
if (field.getArea().equals(area)) {
areaFields.add(field);
}
}
Collections.sort(areaFields);
return areaFields;
}
@Override
public PivotDataSource getDataSource() {
return dataSource;
}
@Override
public void calculate() {
long start = System.currentTimeMillis();
rowsHeaderTree = null;
columnsHeaderTree = null;
getRowsHeaderTree();
long t1 = System.currentTimeMillis();
System.out.println("created rowsHeaderTree in " + (t1 - start));
getColumnsHeaderTree();
long t2 = System.currentTimeMillis();
System.out.println("created columnsHeaderTree in " + (t2 - t1));
t1 = System.currentTimeMillis();
List<PivotField> dataFields = getFields(PivotField.Area.DATA);
calculatedData = new ArrayList<MultiKeyMap>();
for (PivotField field : dataFields) {
field.resetCalculation();
calculatedData.add(getData(field));
}
t2 = System.currentTimeMillis();
System.out.println("filled calculatedData in " + (t2 - t1));
long stop = System.currentTimeMillis();
System.out.println("calculated in " + (stop- start));
System.out.println("calculatedData = " + calculatedData);
// getValues(field, filter)
}
/*
* TODO: trebuie imbunatatita metoda asta. Am facut un test pe un tabel
* cu 4500 inregistrari si 7 coloane (nextreports downloads). Am observat ca
* la 86 chei pe row si 212 chei pe column am 18.232 (86 x 212) combinatii.
* Daca in getValues se sta 3,25 ms (cum am obtinut) rezulta un total de
* 5576 ms. Cred ca ar trebuii sa parcurg o singura data inregistrarile din baza.
*/
private MultiKeyMap getData(PivotField dataField) {
MultiKeyMap data = new MultiKeyMap();
List<List<Object>> rowKeys = getRowKeys();
System.out.println("rowKeys.size() = " + rowKeys.size());
List<List<Object>> columnKeys = getColumnKeys();
System.out.println("columnKeys.size() = " + columnKeys.size());
List<PivotField> rowFields = getFields(PivotField.Area.ROW);
List<PivotField> columnFields = getFields(PivotField.Area.COLUMN);
for (List<Object> rowKey : rowKeys) {
for (List<Object> columnKey : columnKeys) {
Map<Integer, Object> rowFilter = getFilter(rowFields, rowKey);
Map<Integer, Object> columnFilter = getFilter(columnFields, columnKey);
final Map<Integer, Object> filter = new HashMap<Integer, Object>(rowFilter);
filter.putAll(columnFilter);
List<Object> values = getValues(dataField, filter);
if (!CollectionUtils.isEmpty(values) || dataField.getFieldCalculation()!=null) {
/*
System.out.println("filter = " + filter);
System.out.println("values = " + values);
System.out.println(values.size());
*/
Object summary = PivotUtils.getSummary(dataField, values, new FieldValueProvider() {
@Override
public Object getFieldValue(PivotField field) {
List<Object> fieldValues = getValues(field, filter);
return field.getAggregator().init().addAll(fieldValues).getResult();
}
});
// System.out.println("summary = " + summary);
data.put(rowKey, columnKey, summary);
}
}
}
return data;
}
@Override
public Tree getColumnsHeaderTree() {
if (columnsHeaderTree == null) {
Node root = new Node();
insertChildren(root, getFields(PivotField.Area.COLUMN));
columnsHeaderTree = new Tree(root);
}
return columnsHeaderTree;
}
@Override
public Tree getRowsHeaderTree() {
if (rowsHeaderTree == null) {
Node root = new Node();
insertChildren(root, getFields(PivotField.Area.ROW));
rowsHeaderTree = new Tree(root);
}
return rowsHeaderTree;
}
@Override
public List<List<Object>> getRowKeys() {
return TreeHelper.getLeafValues(getRowsHeaderTree().getRoot());
}
@Override
public List<List<Object>> getColumnKeys() {
return TreeHelper.getLeafValues(getColumnsHeaderTree().getRoot());
}
@Override
public Object getValueAt(PivotField dataField, List<Object> rowKey, List<Object> columnKey) {
int index = getFields(PivotField.Area.DATA).indexOf(dataField);
return calculatedData.get(index).get(rowKey, columnKey);
}
@Override
public boolean isShowGrandTotalForColumn() {
return showGrandTotalForColumn;
}
@Override
public void setShowGrandTotalForColumn(boolean showGrandTotalForColumn) {
this.showGrandTotalForColumn = showGrandTotalForColumn;
}
@Override
public boolean isShowGrandTotalForRow() {
return showGrandTotalForRow;
}
@Override
public void setShowGrandTotalForRow(boolean showGrandTotalForRow) {
this.showGrandTotalForRow = showGrandTotalForRow;
}
@Override
public boolean isAutoCalculate() {
return autoCalculate;
}
@Override
public void setAutoCalculate(boolean autoCalculate) {
this.autoCalculate = autoCalculate;
}
@Override
public String toString() {
return "DefaultPivotModel [fields=" + fields + "]";
}
private void insertChildren(Node node, List<PivotField> fields) {
// System.out.println("DefaultPivotModel.insertChildren()");
Set<Object> values = getPossibleChildrenValues(node, fields);
if (CollectionUtils.isEmpty(values)) {
return;
}
Iterator<Object> it = values.iterator();
while (it.hasNext()) {
node.insert(it.next());
}
for (Node child : node.getChildren()) {
insertChildren(child, fields);
}
}
private Set<Object> getPossibleChildrenValues(Node node, List<PivotField> fields) {
int level = node.getLevel();
// System.out.println("level = " + level);
// System.out.println("fields.size = " + fields.size());
if (fields.size() <= level) {
return null;
}
PivotField nextField = fields.get(level);
// System.out.println("nextField = " + nextField);
Map<Integer, Object> filter = getFilter(fields, node.getPathValues());
// System.out.println("filter = " + filter);
Set<Object> values = getUniqueValues(nextField, filter);
// System.out.println("values = " + values);
return values;
}
/*
* Retrieves the values for a data field using a filter.
*/
private List<Object> getValues(PivotField field, Map<Integer, Object> filter) {
if (field.getFieldCalculation() != null)
return Collections.emptyList();
// long start = System.currentTimeMillis();
List<Object> values = new ArrayList<Object>();
final int fieldIndex = field.getIndex();
final int rowCount = dataSource.getRowCount();
if (filter.isEmpty()) {
/*
* No filter -> Just add the values
*/
for (int i = 0; i < rowCount; i++) {
values.add(dataSource.getValueAt(i, fieldIndex));
}
}
else {
/*
* Add all values matching the filter
*/
for (int i = 0; i < rowCount; i++) {
if (acceptValue(i, filter)) {
values.add(dataSource.getValueAt(i, fieldIndex));
}
}
}
// long stop = System.currentTimeMillis();
// System.out.println("getValues in " + (stop - start));
return values;
}
/*
* Retrieves a filter for filtering data source (raw data). The size of fields must be equals with
* the size of values. The key in map is the field index.
*/
private Map<Integer, Object> getFilter(List<PivotField> fields, List<Object> values) {
// long start = System.currentTimeMillis();
Map<Integer, Object> filter = new HashMap<Integer, Object>();
for (int i = 0; i < values.size(); i++) {
int fieldIndex = fields.get(i).getIndex();
// System.out.println(fieldIndex);
filter.put(fieldIndex, values.get(i));
}
// long stop = System.currentTimeMillis();
// System.out.println("getFilter in " + (stop - start));
return filter;
}
private Set<Object> getUniqueValues(PivotField field, Map<Integer, Object> filter) {
List<Object> values = getValues(field, filter);
int sortOrder = field.getSortOrder();
if (sortOrder != PivotField.SORT_ORDER_UNSORTED) {
/*
* We need to get the value set and sort it. We can not use a
* TreeSet here as it does not allow null values.
*/
Set<Object> valueSet = new HashSet<Object>(values);
List<Object> valuesToOrder = new ArrayList<Object>(valueSet);
final int sign = sortOrder == PivotField.SORT_ORDER_ASCENDING ? 1
: sortOrder == PivotField.SORT_ORDER_DESCENDING ? -1 : 1;
Collections.sort(valuesToOrder, new Comparator<Object>() {
@SuppressWarnings("unchecked")
@Override
public int compare(Object o1, Object o2) {
if (o1 == o2)
return 0;
if (o1 == null)
return sign * -1;
if (o2 == null)
return sign * 1;
return sign * ((Comparable<Object>) o1).compareTo(o2);
}
});
return new LinkedHashSet<Object>(valuesToOrder);
}
return new LinkedHashSet<Object>(values);
}
private boolean acceptValue(int row, Map<Integer, Object> filter) {
boolean accept = true;
Set<Integer> keys = filter.keySet();
Object value = null;
for (int index : keys) {
value = dataSource.getValueAt(row, fields.get(index));
Object filterValue = filter.get(index);
if (filterValue != value && (filterValue == null || !filterValue.equals(value))) {
return false;
}
}
return accept;
}
}