/* * JasperReports - Free Java Reporting Library. * Copyright (C) 2001 - 2009 Jaspersoft Corporation. All rights reserved. * http://www.jaspersoft.com * * Unless you have purchased a commercial license agreement from Jaspersoft, * the following license terms apply: * * This program is part of JasperReports. * * JasperReports is free software: you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * JasperReports 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 Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with JasperReports. If not, see <http://www.gnu.org/licenses/>. */ package net.sf.jasperreports.crosstabs.fill.calculation; import java.util.ArrayList; import java.util.Comparator; import java.util.HashMap; import java.util.Iterator; import java.util.LinkedList; import java.util.List; import java.util.ListIterator; import java.util.Map; import java.util.TreeMap; import java.util.TreeSet; import java.util.Map.Entry; import net.sf.jasperreports.crosstabs.fill.calculation.BucketDefinition.Bucket; import net.sf.jasperreports.crosstabs.fill.calculation.MeasureDefinition.MeasureValue; import net.sf.jasperreports.crosstabs.type.CrosstabTotalPositionEnum; import net.sf.jasperreports.engine.JRConstants; import net.sf.jasperreports.engine.JRException; import net.sf.jasperreports.engine.JRRuntimeException; import net.sf.jasperreports.engine.fill.JRCalculable; import net.sf.jasperreports.engine.fill.JRFillCrosstab; import net.sf.jasperreports.engine.type.CalculationEnum; import net.sf.jasperreports.engine.util.JRProperties; /** * Crosstab bucketing engine. * * @author Lucian Chirita (lucianc@users.sourceforge.net) * @version $Id: BucketingService.java 3676 2010-04-02 09:35:07Z shertage $ */ public class BucketingService { public static final String PROPERTY_BUCKET_MEASURE_LIMIT = JRProperties.PROPERTY_PREFIX + "crosstab.bucket.measure.limit"; protected static final byte DIMENSION_ROW = 0; protected static final byte DIMENSION_COLUMN = 1; protected static final int DIMENSIONS = 2; private final JRFillCrosstab fillCrosstab; protected final BucketDefinition[] allBuckets; protected final BucketDefinition[][] buckets; protected final int rowBucketCount; protected final int colBucketCount; protected final boolean[][] retrieveTotal; private boolean[] rowRetrTotals; private int rowRetrTotalMin; private int rowRetrTotalMax; private int[] rowRetrColMax; protected final MeasureDefinition[] measures; protected final int origMeasureCount; protected final int[] measureIndexes; protected final boolean sorted; protected final BucketMap bucketValueMap; protected long dataCount; protected boolean processed; protected HeaderCell[][] colHeaders; protected HeaderCell[][] rowHeaders; protected CrosstabCell[][] cells; private final MeasureValue[] zeroUserMeasureValues; private final int bucketMeasureLimit; private int runningBucketMeasureCount = 0; /** * Creates a crosstab bucketing engine. * * @param fillCrosstab * @param rowBuckets the row bucket definitions * @param columnBuckets the column bucket definitions * @param measures the measure definitions * @param sorted whether the data is presorted * @param retrieveTotal totals to retrieve along with the cell values */ public BucketingService(JRFillCrosstab fillCrosstab, List rowBuckets, List columnBuckets, List measures, boolean sorted, boolean[][] retrieveTotal) { this.fillCrosstab = fillCrosstab; this.sorted = sorted; buckets = new BucketDefinition[DIMENSIONS][]; rowBucketCount = rowBuckets.size(); buckets[DIMENSION_ROW] = new BucketDefinition[rowBucketCount]; rowBuckets.toArray(buckets[DIMENSION_ROW]); colBucketCount = columnBuckets.size(); buckets[DIMENSION_COLUMN] = new BucketDefinition[colBucketCount]; columnBuckets.toArray(buckets[DIMENSION_COLUMN]); allBuckets = new BucketDefinition[rowBucketCount + colBucketCount]; System.arraycopy(buckets[DIMENSION_ROW], 0, allBuckets, 0, rowBucketCount); System.arraycopy(buckets[DIMENSION_COLUMN], 0, allBuckets, rowBucketCount, colBucketCount); origMeasureCount = measures.size(); List measuresList = new ArrayList(measures.size() * 2); List measureIndexList = new ArrayList(measures.size() * 2); for (int i = 0; i < measures.size(); ++i) { MeasureDefinition measure = (MeasureDefinition) measures.get(i); addMeasure(measure, i, measuresList, measureIndexList); } this.measures = new MeasureDefinition[measuresList.size()]; measuresList.toArray(this.measures); this.measureIndexes = new int[measureIndexList.size()]; for (int i = 0; i < measureIndexes.length; ++i) { measureIndexes[i] = ((Integer) measureIndexList.get(i)).intValue(); } this.retrieveTotal = retrieveTotal; checkTotals(); bucketValueMap = createBucketMap(0); zeroUserMeasureValues = initUserMeasureValues(); bucketMeasureLimit = JRProperties.getIntegerProperty(PROPERTY_BUCKET_MEASURE_LIMIT, 0); } protected void checkTotals() { rowRetrTotalMin = rowBucketCount + 1; rowRetrTotalMax = -1; rowRetrTotals = new boolean[rowBucketCount + 1]; rowRetrColMax = new int[rowBucketCount + 1]; for (int row = 0; row <= rowBucketCount; ++row) { rowRetrColMax[row] = -1; boolean total = false; for (int col = 0; col <= colBucketCount; ++col) { if (retrieveTotal[row][col]) { total = true; rowRetrColMax[row] = col; } } rowRetrTotals[row] = total; if (total) { if (row < rowRetrTotalMin) { rowRetrTotalMin = row; } rowRetrTotalMax = row; if (row < rowBucketCount) { allBuckets[row].setComputeTotal(); } } } for (int col = 0; col < colBucketCount; ++col) { BucketDefinition colBucket = allBuckets[rowBucketCount + col]; if (!colBucket.computeTotal()) { boolean total = false; for (int row = 0; !total && row <= rowBucketCount; ++row) { total = retrieveTotal[row][col]; } if (total) { colBucket.setComputeTotal(); } } } for (int d = 0; d < DIMENSIONS; ++d) { boolean dTotal = false; for (int i = 0; i < buckets[d].length; ++i) { if (dTotal) { buckets[d][i].setComputeTotal(); } else { dTotal = buckets[d][i].computeTotal(); } } } } /** * Clears all the accumulated and computed data. */ public void clear() { bucketValueMap.clear(); processed = false; dataCount = 0; runningBucketMeasureCount = 0; } protected BucketMap createBucketMap(int level) { BucketMap map; if (sorted) { map = new BucketListMap(level, false); } else { map = new BucketTreeMap(level); } return map; } protected BucketListMap createCollectBucketMap(int level) { return new BucketListMap(level, true); } protected void addMeasure(MeasureDefinition measure, int index, List measuresList, List measureIndexList) { switch (measure.getCalculation()) { case AVERAGE: case VARIANCE: { MeasureDefinition sumMeasure = MeasureDefinition.createHelperMeasure(measure, CalculationEnum.SUM); addMeasure(sumMeasure, index, measuresList, measureIndexList); MeasureDefinition countMeasure = MeasureDefinition.createHelperMeasure(measure, CalculationEnum.COUNT); addMeasure(countMeasure, index, measuresList, measureIndexList); break; } case STANDARD_DEVIATION: { MeasureDefinition varianceMeasure = MeasureDefinition.createHelperMeasure(measure, CalculationEnum.VARIANCE); addMeasure(varianceMeasure, index, measuresList, measureIndexList); break; } case DISTINCT_COUNT: { MeasureDefinition countMeasure = MeasureDefinition.createDistinctCountHelperMeasure(measure); addMeasure(countMeasure, index, measuresList, measureIndexList); break; } } measuresList.add(measure); measureIndexList.add(Integer.valueOf(index)); } /** * Feeds data to the engine. * * @param bucketValues the bucket values * @param measureValues the measure values * @throws JRException */ public void addData(Object[] bucketValues, Object[] measureValues) throws JRException { if (processed) { throw new JRException("Crosstab data has already been processed."); } ++dataCount; Bucket[] bucketVals = getBucketValues(bucketValues); MeasureValue[] values = bucketValueMap.insertMeasureValues(bucketVals); for (int i = 0; i < measures.length; ++i) { Object measureValue = measureValues[measureIndexes[i]]; values[i].addValue(measureValue); } } protected void bucketMeasuresCreated() { runningBucketMeasureCount += origMeasureCount; checkBucketMeasureCount(runningBucketMeasureCount); } protected Bucket[] getBucketValues(Object[] bucketValues) { Bucket[] bucketVals = new Bucket[allBuckets.length]; for (int i = 0; i < allBuckets.length; ++i) { BucketDefinition bucket = allBuckets[i]; Object value = bucketValues[i]; bucketVals[i] = bucket.create(value); } return bucketVals; } protected MeasureValue[] initMeasureValues() { MeasureValue[] values; values = new MeasureValue[measures.length]; for (int i = 0; i < measures.length; ++i) { MeasureDefinition measure = measures[i]; values[i] = measure.new MeasureValue(); switch (measure.getCalculation()) { case AVERAGE: case VARIANCE: { values[i].setHelper(values[i - 2], JRCalculable.HELPER_SUM); values[i].setHelper(values[i - 1], JRCalculable.HELPER_COUNT); break; } case STANDARD_DEVIATION: { values[i].setHelper(values[i - 1], JRCalculable.HELPER_VARIANCE); } case DISTINCT_COUNT: { values[i].setHelper(values[i - 1], JRCalculable.HELPER_COUNT); } } } return values; } protected MeasureValue[] initUserMeasureValues() { MeasureValue[] vals = new MeasureValue[origMeasureCount]; for (int c = 0, i = 0; i < measures.length; ++i) { if (!measures[i].isSystemDefined()) { vals[c] = measures[i].new MeasureValue(); ++c; } } return vals; } /** * Processes the data which was fed to the engine. * <p> * This method should be called after the data has been exhausted. * The processing consists of total calculations and crosstab table creation. * * @throws JRException */ public void processData() throws JRException { if (!processed) { if (dataCount > 0) { if (allBuckets[rowBucketCount - 1].computeTotal() || allBuckets[allBuckets.length - 1].computeTotal()) { computeTotals(bucketValueMap); } createCrosstab(); } processed = true; } } /** * Checks whether there is any data accumulated by the engine. * * @return <code>true</code> if and only if the engine has any accumulated data */ public boolean hasData() { return dataCount > 0; } /** * Returns the crosstab column headers. * <p> * {@link #processData() processData()} has to be called before this. * * @return the crosstab column headers */ public HeaderCell[][] getColumnHeaders() { return colHeaders; } /** * Returns the crosstab row headers. * <p> * {@link #processData() processData()} has to be called before this. * * @return the crosstab row headers */ public HeaderCell[][] getRowHeaders() { return rowHeaders; } /** * Returns the crosstab data cells. * <p> * {@link #processData() processData()} has to be called before this. * * @return the crosstab data cells */ public CrosstabCell[][] getCrosstabCells() { return cells; } /** * Returns the measure values for a set of bucket values. * * @param bucketValues the bucket values * @return the measure values corresponding to the bucket values */ public MeasureValue[] getMeasureValues(Bucket[] bucketValues) { BucketMap map = bucketValueMap; for (int i = 0; map != null && i < allBuckets.length - 1; ++i) { map = (BucketMap) map.get(bucketValues[i]); } return map == null ? null : (MeasureValue[]) map.get(bucketValues[allBuckets.length - 1]); } protected MeasureValue[] getUserMeasureValues(MeasureValue[] values) { MeasureValue[] vals = new MeasureValue[origMeasureCount]; for (int c = 0, i = 0; i < measures.length; ++i) { if (!measures[i].isSystemDefined()) { vals[c] = values[i]; ++c; } } return vals; } /** * Returns the grand total measure values. * * @return the grand total measure values */ public MeasureValue[] getGrandTotals() { BucketMap map = bucketValueMap; for (int i = 0; map != null && i < allBuckets.length - 1; ++i) { map = (BucketMap) map.getTotalEntry().getValue(); } return map == null ? null : (MeasureValue[]) map.getTotalEntry().getValue(); } protected void computeTotals(BucketMap bucketMap) throws JRException { byte dimension = bucketMap.level < rowBucketCount ? DIMENSION_ROW : DIMENSION_COLUMN; if (dimension == DIMENSION_COLUMN && !allBuckets[allBuckets.length - 1].computeTotal()) { return; } if (!bucketMap.last) { for (Iterator it = bucketMap.entryIterator(); it.hasNext();) { Map.Entry entry = (Map.Entry) it.next(); computeTotals((BucketMap) entry.getValue()); } } if (allBuckets[bucketMap.level].computeTotal()) { if (dimension == DIMENSION_COLUMN) { computeColumnTotal(bucketMap); } else { computeRowTotals(bucketMap); } } } protected void sumVals(MeasureValue[] totals, MeasureValue[] vals) throws JRException { for (int i = 0; i < measures.length; i++) { totals[i].addValue(vals[i]); } } protected void computeColumnTotal(BucketMap bucketMap) throws JRException { MeasureValue[] totals = initMeasureValues(); for (Iterator it = bucketMap.entryIterator(); it.hasNext();) { Map.Entry entry = (Map.Entry) it.next(); for (int i = bucketMap.level + 1; i < allBuckets.length; ++i) { entry = ((BucketMap) entry.getValue()).getTotalEntry(); } sumVals(totals, (MeasureValue[]) entry.getValue()); } for (int i = bucketMap.level + 1; i < allBuckets.length; ++i) { bucketMap = bucketMap.addTotalNextMap(); } bucketMap.addTotalEntry(totals); } protected void computeRowTotals(BucketMap bucketMap) throws JRException { BucketListMap totals = createCollectBucketMap(rowBucketCount); for (Iterator it = bucketMap.entryIterator(); it.hasNext();) { Map.Entry entry = (Map.Entry) it.next(); for (int i = bucketMap.level + 1; i < rowBucketCount; ++i) { entry = ((BucketMap) entry.getValue()).getTotalEntry(); } totals.collectVals((BucketMap) entry.getValue(), true); } BucketMap totalBucketMap = bucketMap; for (int i = bucketMap.level + 1; i < rowBucketCount; ++i) { totalBucketMap = totalBucketMap.addTotalNextMap(); } totalBucketMap.addTotalEntry(totals); } static protected class MapEntry implements Map.Entry, Comparable { final Bucket key; final Object value; MapEntry(Bucket key, Object value) { this.key = key; this.value = value; } public Object getKey() { return key; } public Object getValue() { return value; } public Object setValue(Object value) { throw new UnsupportedOperationException(); } public int compareTo(Object o) { return key.compareTo(((MapEntry) o).key); } public String toString() { return key + "=" + value; } } protected abstract class BucketMap { final int level; final boolean last; final Bucket totalKey; BucketMap(int level) { this.level = level; this.last = level == allBuckets.length - 1; totalKey = allBuckets[level].VALUE_TOTAL; } BucketMap addTotalNextMap() { BucketMap nextMap = createBucketMap(level + 1); addTotalEntry(nextMap); return nextMap; } abstract void clear(); abstract Iterator entryIterator(); abstract Object get(Bucket key); abstract MeasureValue[] insertMeasureValues(Bucket[] bucketValues); /* abstract void fillKeys(Collection collectedKeys);*/ abstract void addTotalEntry(Object val); abstract int size(); abstract Map.Entry getTotalEntry(); } protected class BucketTreeMap extends BucketMap { TreeMap map; BucketTreeMap(int level) { super(level); map = new TreeMap(); } void clear() { map.clear(); } Iterator entryIterator() { return map.entrySet().iterator(); } Object get(Bucket key) { return map.get(key); } MeasureValue[] insertMeasureValues(Bucket[] bucketValues) { BucketTreeMap levelMap = (BucketTreeMap) bucketValueMap; for (int i = 0; i < bucketValues.length - 1; i++) { BucketTreeMap nextMap = (BucketTreeMap) levelMap.get(bucketValues[i]); if (nextMap == null) { nextMap = new BucketTreeMap(i + 1); levelMap.map.put(bucketValues[i], nextMap); } levelMap = nextMap; } MeasureValue[] values = (MeasureValue[]) levelMap.get(bucketValues[bucketValues.length - 1]); if (values == null) { values = initMeasureValues(); levelMap.map.put(bucketValues[bucketValues.length - 1], values); bucketMeasuresCreated(); } return values; } int size() { return map.size(); } void addTotalEntry(Object value) { map.put(totalKey, value); } Map.Entry getTotalEntry() { Object value = get(totalKey); return value == null ? null : new MapEntry(totalKey, value); } public String toString() { return map.toString(); } } protected class BucketListMap extends BucketMap { List entries; // we maintain a map as well in order to have fast search by key // TODO implement this in a single structure Map entryMap; BucketListMap(int level, boolean linked) { super(level); if (linked) { entries = new LinkedList(); } else { entries = new ArrayList(); } entryMap = new HashMap(); } void clear() { entries.clear(); entryMap.clear(); } Iterator entryIterator() { return entries.iterator(); } private void add(Bucket key, Object value) { entries.add(new MapEntry(key, value)); entryMap.put(key, value); } Object get(Bucket key) { return entryMap.get(key); } MeasureValue[] insertMeasureValues(Bucket[] bucketValues) { int i = 0; Object levelObj = this; BucketListMap map = null; while (i < allBuckets.length) { map = (BucketListMap) levelObj; int size = map.entries.size(); if (size == 0) { break; } MapEntry lastEntry = (MapEntry) map.entries.get(size - 1); if (!lastEntry.key.equals(bucketValues[i])) { break; } ++i; levelObj = lastEntry.value; } if (i == allBuckets.length) { return (MeasureValue[]) levelObj; } while (i < allBuckets.length - 1) { BucketListMap nextMap = new BucketListMap(i + 1, false); map.add(bucketValues[i], nextMap); map = nextMap; ++i; } MeasureValue[] values = initMeasureValues(); map.add(bucketValues[i], values); bucketMeasuresCreated(); return values; } int size() { return entries.size(); } void addTotalEntry(Object value) { add(totalKey, value); } Map.Entry getTotalEntry() { MapEntry lastEntry = (MapEntry) entries.get(entries.size() - 1); if (lastEntry.key.isTotal()) { return lastEntry; } return null; } void collectVals(BucketMap map, boolean sum) throws JRException { ListIterator totalIt = entries.listIterator(); MapEntry totalItEntry = totalIt.hasNext() ? (MapEntry) totalIt.next() : null; Iterator it = map.entryIterator(); Map.Entry entry = it.hasNext() ? (Map.Entry) it.next() : null; while(entry != null) { Bucket key = (Bucket) entry.getKey(); int compare = totalItEntry == null ? -1 : key.compareTo(totalItEntry.key); if (compare <= 0) { Object addVal = null; if (last) { if (sum) { MeasureValue[] totalVals = compare == 0 ? (MeasureValue[]) totalItEntry.value : null; if (totalVals == null) { totalVals = initMeasureValues(); addVal = totalVals; } sumVals(totalVals, (MeasureValue[]) entry.getValue()); } } else { BucketListMap nextTotals = compare == 0 ? (BucketListMap) totalItEntry.value : null; if (nextTotals == null) { nextTotals = createCollectBucketMap(level + 1); addVal = nextTotals; } nextTotals.collectVals((BucketMap) entry.getValue(), sum); } if (compare < 0) { if (totalItEntry != null) { totalIt.previous(); } totalIt.add(new MapEntry(key, addVal)); entryMap.put(key, addVal); if (totalItEntry != null) { totalIt.next(); } } entry = it.hasNext() ? (Map.Entry) it.next() : null; } if (compare >= 0) { totalItEntry = totalIt.hasNext() ? (MapEntry) totalIt.next() : null; } } } public String toString() { StringBuffer sb = new StringBuffer(); sb.append('{'); for (Iterator it = entries.iterator(); it.hasNext();) { MapEntry entry = (MapEntry) it.next(); sb.append(entry); if (it.hasNext()) { sb.append(", "); } } sb.append('}'); return sb.toString(); } } protected void createCrosstab() throws JRException { CollectedList[] collectedHeaders = new CollectedList[BucketingService.DIMENSIONS]; collectedHeaders[DIMENSION_ROW] = createHeadersList(DIMENSION_ROW, bucketValueMap, 0, false); BucketListMap collectedCols; if (allBuckets[0].computeTotal()) { BucketMap map = bucketValueMap; for (int i = 0; i < rowBucketCount; ++i) { map = (BucketMap) map.getTotalEntry().getValue(); } collectedCols = (BucketListMap) map; } else { collectedCols = createCollectBucketMap(rowBucketCount); collectCols(collectedCols, bucketValueMap); } collectedHeaders[DIMENSION_COLUMN] = createHeadersList(DIMENSION_COLUMN, collectedCols, 0, false); int rowBuckets = collectedHeaders[BucketingService.DIMENSION_ROW].span; int colBuckets = collectedHeaders[BucketingService.DIMENSION_COLUMN].span; int bucketMeasureCount = rowBuckets * colBuckets * origMeasureCount; checkBucketMeasureCount(bucketMeasureCount); colHeaders = createHeaders(BucketingService.DIMENSION_COLUMN, collectedHeaders); rowHeaders = createHeaders(BucketingService.DIMENSION_ROW, collectedHeaders); cells = new CrosstabCell[rowBuckets][colBuckets]; fillCells(collectedHeaders, bucketValueMap, 0, new int[]{0, 0}, new ArrayList(), new ArrayList()); } protected void checkBucketMeasureCount(int bucketMeasureCount) { if (bucketMeasureLimit > 0 && bucketMeasureCount > bucketMeasureLimit) { throw new JRRuntimeException("Crosstab bucket/measure limit (" + bucketMeasureLimit + ") exceeded."); } } protected void collectCols(BucketListMap collectedCols, BucketMap bucketMap) throws JRException { if (allBuckets[bucketMap.level].computeTotal()) { BucketMap map = bucketMap; for (int i = bucketMap.level; i < rowBucketCount; ++i) { map = (BucketMap) map.getTotalEntry().getValue(); } collectedCols.collectVals(map, false); return; } for (Iterator it = bucketMap.entryIterator(); it.hasNext();) { Map.Entry entry = (Map.Entry) it.next(); BucketMap nextMap = (BucketMap) entry.getValue(); if (bucketMap.level == rowBucketCount - 1) { collectedCols.collectVals(nextMap, false); } else { collectCols(collectedCols, nextMap); } } } protected CollectedList createHeadersList(byte dimension, BucketMap bucketMap, int level, boolean total) throws JRException { BucketDefinition bucketDefinition = allBuckets[bucketMap.level]; CrosstabTotalPositionEnum totalPosition = bucketDefinition.getTotalPosition(); CollectedList headers; if (bucketDefinition.hasOrderValues()) { headers = new OrderedCollectedList(bucketDefinition); } else { headers = new SequentialCollectedList(totalPosition); } for (Iterator it = bucketMap.entryIterator(); it.hasNext();) { Map.Entry entry = (Map.Entry) it.next(); Bucket bucketValue = (Bucket) entry.getKey(); boolean totalBucket = bucketValue.isTotal(); boolean createHeader = !totalBucket || total || totalPosition != CrosstabTotalPositionEnum.NONE; if (createHeader) { CollectedList nextHeaders; if (level + 1 < buckets[dimension].length) { BucketMap nextMap = (BucketMap) entry.getValue(); nextHeaders = createHeadersList(dimension, nextMap, level + 1, total || totalBucket); } else { nextHeaders = new SequentialCollectedList(CrosstabTotalPositionEnum.NONE); nextHeaders.span = 1; } nextHeaders.key = bucketValue; if (bucketDefinition.hasOrderValues()) { Object orderValue = evaluateOrderValue(bucketMap, bucketValue); nextHeaders.orderValue = orderValue; } headers.add(nextHeaders); } } if (headers.span == 0) { headers.span = 1; } return headers; } protected Object evaluateOrderValue(BucketMap bucketMap, Bucket bucket) throws JRException { Object bucketValue = bucketMap.get(bucket); for (int idx = bucketMap.level + 1; idx < rowBucketCount + colBucketCount; ++idx) { bucketValue = ((BucketMap) bucketValue).getTotalEntry().getValue(); } MeasureValue[] totals = (MeasureValue[]) bucketValue; MeasureValue[] userTotals = getUserMeasureValues(totals); return fillCrosstab.evaluateExpression( allBuckets[bucketMap.level].getOrderByExpression(), userTotals); } protected HeaderCell[][] createHeaders(byte dimension, CollectedList[] headersLists) { HeaderCell[][] headers = new HeaderCell[buckets[dimension].length][headersLists[dimension].span]; List vals = new ArrayList(); fillHeaders(dimension, headers, 0, 0, headersLists[dimension], vals); return headers; } protected void fillHeaders(byte dimension, HeaderCell[][] headers, int level, int col, CollectedList list, List vals) { if (level == buckets[dimension].length) { return; } for (Iterator it = list.iterator(); it.hasNext();) { CollectedList subList = (CollectedList) it.next(); vals.add(subList.key); int depthSpan = subList.key.isTotal() ? buckets[dimension].length - level : 1; Bucket[] values = new Bucket[buckets[dimension].length]; vals.toArray(values); headers[level][col] = new HeaderCell(values, subList.span, depthSpan); if (!subList.key.isTotal()) { fillHeaders(dimension, headers, level + 1, col, subList, vals); } col += subList.span; vals.remove(vals.size() - 1); } } protected void fillCells(CollectedList[] collectedHeaders, BucketMap bucketMap, int level, int[] pos, List vals, List bucketMaps) { bucketMaps.add(bucketMap); byte dimension = level < rowBucketCount ? DIMENSION_ROW : DIMENSION_COLUMN; boolean last = level == allBuckets.length - 1; CollectedList[] nextCollected = null; if (!last) { nextCollected = new CollectedList[DIMENSIONS]; for (int d = 0; d < DIMENSIONS; ++d) { if (d != dimension) { nextCollected[d] = collectedHeaders[d]; } } } boolean incrementRow = level == buckets[BucketingService.DIMENSION_ROW].length - 1; CollectedList collectedList = collectedHeaders[dimension]; for (Iterator it = collectedList.iterator(); it.hasNext();) { CollectedList list = (CollectedList) it.next(); Object bucketValue = bucketMap == null ? null : bucketMap.get(list.key); vals.add(list.key); if (last) { fillCell(pos, vals, bucketMaps, (MeasureValue[]) bucketValue); } else { nextCollected[dimension] = list; BucketMap nextMap = bucketValue == null ? null : (BucketMap) bucketValue; fillCells(nextCollected, nextMap, level + 1, pos, vals, bucketMaps); } vals.remove(vals.size() - 1); if (incrementRow) { ++pos[0]; pos[1] = 0; } } bucketMaps.remove(bucketMaps.size() - 1); } protected void fillCell(int[] pos, List vals, List bucketMaps, MeasureValue[] values) { Iterator valsIt = vals.iterator(); Bucket[] rowValues = new Bucket[buckets[BucketingService.DIMENSION_ROW].length]; for (int i = 0; i < rowValues.length; i++) { rowValues[i] = (Bucket) valsIt.next(); } Bucket[] columnValues = new Bucket[buckets[BucketingService.DIMENSION_COLUMN].length]; for (int i = 0; i < columnValues.length; i++) { columnValues[i] = (Bucket) valsIt.next(); } MeasureValue[] measureVals = values == null ? zeroUserMeasureValues : getUserMeasureValues(values); MeasureValue[][][] totals = retrieveTotals(vals, bucketMaps); cells[pos[0]][pos[1]] = new CrosstabCell(rowValues, columnValues, measureVals, totals); ++pos[1]; } protected MeasureValue[][][] retrieveTotals(List vals, List bucketMaps) { MeasureValue[][][] totals = new MeasureValue[rowBucketCount + 1][colBucketCount + 1][]; for (int row = rowRetrTotalMax; row >= rowRetrTotalMin; --row) { if (!rowRetrTotals[row]) { continue; } BucketMap rowMap = (BucketMap) bucketMaps.get(row); for (int i = row; rowMap != null && i < rowBucketCount; ++i) { Entry totalEntry = rowMap.getTotalEntry(); rowMap = totalEntry == null ? null : (BucketMap) totalEntry.getValue(); } for (int col = 0; col <= rowRetrColMax[row]; ++col) { BucketMap colMap = rowMap; if (col < colBucketCount - 1) { if (row == rowBucketCount) { rowMap = (BucketMap) bucketMaps.get(rowBucketCount + col + 1); } else if (rowMap != null) { rowMap = (BucketMap) rowMap.get((Bucket) vals.get(rowBucketCount + col)); } } if (!retrieveTotal[row][col]) { continue; } for (int i = col + 1; colMap != null && i < colBucketCount; ++i) { colMap = (BucketMap) colMap.getTotalEntry().getValue(); } if (colMap != null) { if (col == colBucketCount) { MeasureValue[] measureValues = (MeasureValue[]) colMap.get((Bucket) vals.get(rowBucketCount + colBucketCount - 1)); if (measureValues != null) { totals[row][col] = getUserMeasureValues(measureValues); } } else { Map.Entry totalEntry = colMap.getTotalEntry(); if (totalEntry != null) { MeasureValue[] totalValues = (MeasureValue[]) totalEntry.getValue(); totals[row][col] = getUserMeasureValues(totalValues); } } } if (totals[row][col] == null) { totals[row][col] = zeroUserMeasureValues; } } } return totals; } protected static abstract class CollectedList { private static final long serialVersionUID = JRConstants.SERIAL_VERSION_UID; int span; Bucket key; Object orderValue; CollectedList() { span = 0; } public abstract Iterator iterator(); public void add(CollectedList sublist) { addSublist(sublist); incrementSpan(sublist); } protected abstract void addSublist(CollectedList sublist); private void incrementSpan(CollectedList sublist) { if (sublist != null) { span += sublist.span; } else { span += 1; } } public String toString() { return key + "/" + span + ": " + super.toString(); } } protected static class SequentialCollectedList extends CollectedList { private static final long serialVersionUID = JRConstants.SERIAL_VERSION_UID; final CrosstabTotalPositionEnum totalPosition; final LinkedList list; SequentialCollectedList(CrosstabTotalPositionEnum totalPosition) { this.totalPosition = totalPosition; list = new LinkedList(); } public Iterator iterator() { return list.iterator(); } protected void addSublist(CollectedList sublist) { if (sublist.key.isTotal() && totalPosition == CrosstabTotalPositionEnum.START) { list.addFirst(sublist); } else { list.add(sublist); } } } protected static class OrderedCollectedList extends CollectedList { private static final long serialVersionUID = JRConstants.SERIAL_VERSION_UID; final TreeSet list; OrderedCollectedList(BucketDefinition bucketDefinition) { super(); CollectedListComparator comparator = new CollectedListComparator(bucketDefinition); list = new TreeSet(comparator); } public Iterator iterator() { return list.iterator(); } protected void addSublist(CollectedList sublist) { list.add(sublist); } } protected static class CollectedListComparator implements Comparator { final BucketDefinition bucketDefinition; final boolean totalFirst; CollectedListComparator(BucketDefinition bucketDefinition) { this.bucketDefinition = bucketDefinition; this.totalFirst = bucketDefinition.getTotalPosition() == CrosstabTotalPositionEnum.START; } public int compare(Object o1, Object o2) { if (o1 == o2) { return 0; } CollectedList l1 = (CollectedList) o1; CollectedList l2 = (CollectedList) o2; int order; if (l1.key.isTotal()) { if (l2.key.isTotal()) { // this should not happen throw new JRRuntimeException("Two total keys in the same list"); } order = totalFirst ? -1 : 1; } else if (l2.key.isTotal()) { order = totalFirst ? 1 : -1; } else { // first compare the order values order = bucketDefinition.compareOrderValues( l1.orderValue, l2.orderValue); if (order == 0) { // if order values are equal, fallback to bucket value order order = l1.key.compareTo(l2.key); } } return order; } } }