net.sf.jasperreports.engine.analytics.dataset.MultiAxisDataService.java Source code

Java tutorial

Introduction

Here is the source code for net.sf.jasperreports.engine.analytics.dataset.MultiAxisDataService.java

Source

/*
 * JasperReports - Free Java Reporting Library.
 * Copyright (C) 2001 - 2019 TIBCO Software Inc. 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.engine.analytics.dataset;

import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.ListIterator;
import java.util.Map;
import java.util.Map.Entry;

import net.sf.jasperreports.crosstabs.fill.calculation.BucketDefinition;
import net.sf.jasperreports.crosstabs.fill.calculation.BucketDefinition.Bucket;
import net.sf.jasperreports.crosstabs.fill.calculation.BucketingService;
import net.sf.jasperreports.crosstabs.fill.calculation.BucketingService.BucketMap;
import net.sf.jasperreports.crosstabs.fill.calculation.BucketingServiceContext;
import net.sf.jasperreports.crosstabs.fill.calculation.MeasureDefinition;
import net.sf.jasperreports.crosstabs.fill.calculation.MeasureDefinition.MeasureValue;
import net.sf.jasperreports.crosstabs.type.CrosstabTotalPositionEnum;
import net.sf.jasperreports.engine.JRException;
import net.sf.jasperreports.engine.JRExpression;
import net.sf.jasperreports.engine.JRRuntimeException;
import net.sf.jasperreports.engine.JasperReportsContext;
import net.sf.jasperreports.engine.analytics.data.Axis;
import net.sf.jasperreports.engine.analytics.data.AxisLevel;
import net.sf.jasperreports.engine.analytics.data.AxisLevel.Type;
import net.sf.jasperreports.engine.analytics.data.AxisLevelNode;
import net.sf.jasperreports.engine.analytics.data.MappedPropertyValues;
import net.sf.jasperreports.engine.analytics.data.Measure;
import net.sf.jasperreports.engine.analytics.data.MultiAxisDataSource;
import net.sf.jasperreports.engine.analytics.data.PropertyValues;
import net.sf.jasperreports.engine.analytics.data.StandardAxisLevel;
import net.sf.jasperreports.engine.analytics.data.StandardMeasure;
import net.sf.jasperreports.engine.analytics.data.StandardMeasureValue;
import net.sf.jasperreports.engine.fill.JRCalculator;
import net.sf.jasperreports.engine.fill.JRDefaultIncrementerFactory;
import net.sf.jasperreports.engine.fill.JRExpressionEvalException;
import net.sf.jasperreports.engine.fill.JRExtendedIncrementerFactory;
import net.sf.jasperreports.engine.fill.JRFillExpressionEvaluator;
import net.sf.jasperreports.engine.fill.JRIncrementerFactoryCache;
import net.sf.jasperreports.engine.util.SingleValue;
import net.sf.jasperreports.engine.util.ValuePropertiesWrapper;
import net.sf.jasperreports.engine.util.ValuePropertiesWrapperComparator;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

/**
 * @author Lucian Chirita (lucianc@users.sourceforge.net)
 */
public class MultiAxisDataService {

    protected static final Log log = LogFactory.getLog(MultiAxisDataService.class);

    public static final String EXCEPTION_MESSAGE_KEY_INCREMENT_BIDIMENSIONAL_DATASET_ERROR = "engine.analytics.dataset.increment.bidimensional.dataset.error";

    private final BucketingServiceContext serviceContext;
    private final MultiAxisData data;
    private final MultiAxisBucketingService bucketingService;

    private final Map<DataAxisLevel, Map<String, Integer>> axisLevelBucketPropertyIndexes = new HashMap<DataAxisLevel, Map<String, Integer>>();

    private final List<List<AxisLevel>> axisLevels;
    private final Bucket[] axisRoots;
    private final List<Measure> measures;

    private final Object[] bucketValues;
    private final Object[] measureValues;

    public MultiAxisDataService(JasperReportsContext jasperReportsContext,
            JRFillExpressionEvaluator expressionEvaluator, MultiAxisData data, byte evaluation) throws JRException {
        this.serviceContext = new ServiceContext(jasperReportsContext, expressionEvaluator);
        this.data = data;

        if (log.isDebugEnabled()) {
            log.debug("creating multi axis data service for " + data);
        }

        this.axisLevels = new ArrayList<List<AxisLevel>>(Axis.axisCount());
        this.axisLevels.addAll(Collections.<List<AxisLevel>>nCopies(Axis.axisCount(), null));

        List<List<BucketDefinition>> axisBuckets = new ArrayList<List<BucketDefinition>>(Axis.axisCount());
        axisBuckets.addAll(Collections.<List<BucketDefinition>>nCopies(Axis.axisCount(), null));

        this.axisRoots = new Bucket[Axis.axisCount()];

        for (Axis axis : Axis.values()) {
            DataAxis dataAxis = data.getDataAxis(axis);
            List<DataAxisLevel> dataLevels = dataAxis == null ? Collections.<DataAxisLevel>emptyList()
                    : dataAxis.getLevels();

            int levelCount = axisLevels.size();

            List<AxisLevel> levels = new ArrayList<AxisLevel>(levelCount + 1);
            levels.add(createRootLevel(axis));

            List<BucketDefinition> buckets = new ArrayList<BucketDefinition>(levelCount + 1);
            BucketDefinition rowRootBucket = createRootBucket();
            buckets.add(rowRootBucket);
            axisRoots[axis.ordinal()] = rowRootBucket.create(SingleValue.VALUE);

            for (DataAxisLevel dataLevel : dataLevels) {
                // create the data source level
                AxisLevel level = createLevel(axis, dataLevel, evaluation, levels.size());
                levels.add(level);
                // create the bucket
                buckets.add(createServiceBucket(dataLevel, evaluation));

                if (log.isDebugEnabled()) {
                    log.debug("created level " + level);
                }
            }

            this.axisLevels.set(axis.ordinal(), levels);
            axisBuckets.set(axis.ordinal(), buckets);
        }

        List<DataMeasure> dataMeasures = data.getMeasures();
        this.measures = new ArrayList<Measure>(dataMeasures.size());
        List<MeasureDefinition> measureList = new ArrayList<MeasureDefinition>(dataMeasures.size());
        for (DataMeasure dataMeasure : dataMeasures) {
            // create the data source measure
            Measure measure = createMeasure(dataMeasure, evaluation);
            this.measures.add(measure);
            // create the bucketing measure
            measureList.add(createServiceMeasure(dataMeasure));

            if (log.isDebugEnabled()) {
                log.debug("created measure " + measure);
            }
        }

        // compute all totals

        List<BucketDefinition> rowBuckets = axisBuckets.get(Axis.ROWS.ordinal());
        if (rowBuckets.size() > 1) {
            rowBuckets.get(1).setComputeTotal();
        }
        List<BucketDefinition> colBuckets = axisBuckets.get(Axis.COLUMNS.ordinal());
        colBuckets.get(0).setComputeTotal();

        bucketValues = new Object[rowBuckets.size() + colBuckets.size()];
        measureValues = new Object[dataMeasures.size()];

        // all false
        boolean[][] retrieveTotal = new boolean[rowBuckets.size() + 1][colBuckets.size() + 2];
        bucketingService = new MultiAxisBucketingService(rowBuckets, colBuckets, measureList, retrieveTotal);
    }

    protected AxisLevel createRootLevel(Axis axis) throws JRException {
        StandardAxisLevel level = new StandardAxisLevel();
        level.setAxis(axis);
        level.setType(Type.ROOT);
        level.setName(StandardAxisLevel.ROOT_LEVEL_NAME);
        level.setLabel(StandardAxisLevel.ROOT_LEVEL_NAME);
        level.setValueType(SingleValue.class);
        level.setDepth(0);
        return level;
    }

    protected BucketDefinition createRootBucket() throws JRException {
        return new BucketDefinition(SingleValue.class, null, null, BucketOrder.ASCENDING,
                CrosstabTotalPositionEnum.START);
    }

    protected AxisLevel createLevel(Axis axis, DataAxisLevel dataLevel, byte evaluation, int depth)
            throws JRException {
        StandardAxisLevel level = new StandardAxisLevel();
        level.setAxis(axis);
        level.setName(dataLevel.getName());
        String label = (String) serviceContext.getExpressionEvaluator().evaluate(dataLevel.getLabelExpression(),
                evaluation);
        level.setLabel(label);
        level.setValueType(dataLevel.getBucket().getValueClass());
        level.setDepth(depth);
        return level;
    }

    protected BucketDefinition createServiceBucket(DataAxisLevel level, byte evaluation) throws JRException {
        DataLevelBucket bucket = level.getBucket();
        Class<?> valueClass = bucket.getValueClass();

        Comparator<Object> comparator = null;
        JRExpression comparatorExpression = bucket.getComparatorExpression();
        if (comparatorExpression != null) {
            comparator = (Comparator<Object>) serviceContext.getExpressionEvaluator().evaluate(comparatorExpression,
                    evaluation);
        }

        BucketDefinition bucketDefinition;
        List<DataLevelBucketProperty> bucketProperties = bucket.getBucketProperties();
        if (bucket.getLabelExpression() != null || (bucketProperties != null && !bucketProperties.isEmpty())) {
            // wrapping values in a ValuePropertiesWrapper in order to store property values
            Map<String, Integer> propertyIndexes = new LinkedHashMap<String, Integer>();
            for (ListIterator<DataLevelBucketProperty> it = bucketProperties.listIterator(); it.hasNext();) {
                DataLevelBucketProperty bucketProperty = it.next();
                propertyIndexes.put(bucketProperty.getName(), it.previousIndex());
            }
            axisLevelBucketPropertyIndexes.put(level, propertyIndexes);

            bucketDefinition = new PropertiesWrapperBucketDefintion(comparator, bucket.getOrder(),
                    CrosstabTotalPositionEnum.START);
        } else {
            bucketDefinition = new BucketDefinition(valueClass, null, comparator, bucket.getOrder(),
                    CrosstabTotalPositionEnum.START);
        }

        return bucketDefinition;
    }

    protected Measure createMeasure(DataMeasure dataMeasure, byte evaluation) throws JRException {
        StandardMeasure measure = new StandardMeasure();
        measure.setName(dataMeasure.getName());
        String label = (String) serviceContext.getExpressionEvaluator().evaluate(dataMeasure.getLabelExpression(),
                evaluation);
        measure.setLabel(label);
        measure.setValueType(dataMeasure.getValueClass());
        return measure;
    }

    protected MeasureDefinition createServiceMeasure(DataMeasure measure) {
        JRExtendedIncrementerFactory incrFactory;
        String incrementerFactoryClassName = measure.getIncrementerFactoryClassName();
        if (incrementerFactoryClassName == null) {
            incrFactory = JRDefaultIncrementerFactory.getFactory(measure.getValueClass());
        } else {
            incrFactory = (JRExtendedIncrementerFactory) JRIncrementerFactoryCache
                    .getInstance(measure.getIncrementerFactoryClass());
        }

        return new MeasureDefinition(measure.getValueClass(), measure.getCalculation(), incrFactory);
    }

    public void evaluateRecord(JRCalculator calculator) throws JRExpressionEvalException {
        int bucketIdx = 0;
        bucketValues[bucketIdx++] = SingleValue.VALUE;

        DataAxis rowAxis = data.getDataAxis(Axis.ROWS);
        if (rowAxis != null) {
            for (DataAxisLevel level : rowAxis.getLevels()) {
                bucketValues[bucketIdx++] = evaluateBucketValue(calculator, level);
            }
        }

        bucketValues[bucketIdx++] = SingleValue.VALUE;

        DataAxis colAxis = data.getDataAxis(Axis.COLUMNS);
        if (colAxis != null) {
            for (DataAxisLevel level : colAxis.getLevels()) {
                bucketValues[bucketIdx++] = evaluateBucketValue(calculator, level);
            }
        }

        int measureIdx = 0;
        for (DataMeasure measure : data.getMeasures()) {
            measureValues[measureIdx++] = calculator.evaluate(measure.getValueExpression());
        }
    }

    protected Object evaluateBucketValue(JRCalculator calculator, DataAxisLevel level)
            throws JRExpressionEvalException {
        DataLevelBucket bucket = level.getBucket();
        Object mainValue = calculator.evaluate(bucket.getExpression());

        JRExpression labelExpression = bucket.getLabelExpression();
        List<DataLevelBucketProperty> bucketProperties = bucket.getBucketProperties();
        Object bucketValue;
        if (labelExpression == null && (bucketProperties == null || bucketProperties.isEmpty())) {
            bucketValue = mainValue;
        } else {
            String label = labelExpression == null ? null : (String) calculator.evaluate(labelExpression);

            Object[] propertyValues;
            if (bucketProperties == null || bucketProperties.isEmpty()) {
                propertyValues = null;
            } else {
                // evaluate property values
                //FIXME avoid evaluating these for each record
                propertyValues = new Object[bucketProperties.size()];
                for (ListIterator<DataLevelBucketProperty> it = bucketProperties.listIterator(); it.hasNext();) {
                    DataLevelBucketProperty bucketProperty = it.next();
                    propertyValues[it.previousIndex()] = calculator.evaluate(bucketProperty.getExpression());
                }
            }

            // wrap the main value and property values together
            bucketValue = new ValuePropertiesWrapper(mainValue, label, propertyValues);
        }

        return bucketValue;
    }

    public void addRecord() {
        try {
            bucketingService.addData(bucketValues, measureValues);
        } catch (JRException e) {
            throw new JRRuntimeException(EXCEPTION_MESSAGE_KEY_INCREMENT_BIDIMENSIONAL_DATASET_ERROR,
                    (Object[]) null, e);
        }
    }

    public void clearData() {
        if (log.isDebugEnabled()) {
            log.debug("clearing data");
        }

        bucketingService.clear();
    }

    public MultiAxisDataSource createDataSource() throws JRException {
        if (log.isDebugEnabled()) {
            log.debug("creating multi axis data source");
        }

        // TODO lucianc do we need a final increment?
        bucketingService.processData();

        return new DataSource();
    }

    protected class MultiAxisBucketingService extends BucketingService {
        public MultiAxisBucketingService(List<BucketDefinition> rowBuckets, List<BucketDefinition> columnBuckets,
                List<MeasureDefinition> measures, boolean[][] retrieveTotal) {
            super(MultiAxisDataService.this.serviceContext, rowBuckets, columnBuckets, measures, false,
                    retrieveTotal);
        }

        protected BucketMap getRowRootChildren() {
            if (axisLevels.get(Axis.ROWS.ordinal()).size() == 1) {
                // only root level
                return null;
            }

            return (BucketMap) bucketValueMap.get(axisRoots[Axis.ROWS.ordinal()]);
        }

        protected BucketMap getColumnRootChildren() {
            if (axisLevels.get(Axis.COLUMNS.ordinal()).size() == 1) {
                // only root level
                return null;
            }

            BucketMap bucketMap = (BucketMap) bucketValueMap.get(axisRoots[Axis.ROWS.ordinal()]);
            // descend on row total nodes
            int rowLevelsCount = axisLevels.get(Axis.ROWS.ordinal()).size();
            for (int idx = 1; bucketMap != null && idx < rowLevelsCount; ++idx) {
                bucketMap = (BucketMap) bucketMap.getTotalEntry().getValue();
            }

            if (bucketMap == null) {
                // this can happen when there was no data
                return null;
            }

            return (BucketMap) bucketMap.get(axisRoots[Axis.COLUMNS.ordinal()]);
        }
    }

    protected class DataSource implements MultiAxisDataSource {
        private final List<List<AxisLevel>> axisDataLevels;
        public static final String EXCEPTION_MESSAGE_KEY_UNKNOWN_AXIS = "engine.analytics.dataset.unknown.axis";

        public DataSource() {
            axisDataLevels = new ArrayList<List<AxisLevel>>(axisLevels.size());
            for (List<AxisLevel> levels : axisLevels) {
                List<AxisLevel> dataLevels = Collections.unmodifiableList(levels.subList(1, levels.size()));
                axisDataLevels.add(dataLevels);
            }
        }

        @Override
        public List<? extends AxisLevel> getAxisLevels(Axis axis) {
            return axisDataLevels.get(axis.ordinal());
        }

        @Override
        public List<? extends Measure> getMeasures() {
            return measures;
        }

        @Override
        public AxisLevelNode getAxisRootNode(Axis axis) {
            BucketMap rootChildren;
            switch (axis) {
            case ROWS:
                rootChildren = bucketingService.getRowRootChildren();
                break;
            case COLUMNS:
                rootChildren = bucketingService.getColumnRootChildren();
                break;
            default:
                throw new JRRuntimeException(EXCEPTION_MESSAGE_KEY_UNKNOWN_AXIS, new Object[] { axis });
            }

            Bucket rootBucket = axisRoots[axis.ordinal()];
            return new LevelNode(axis, 0, null, rootBucket, rootChildren);
        }

        @Override
        public List<? extends net.sf.jasperreports.engine.analytics.data.MeasureValue> getMeasureValues(
                AxisLevelNode... nodes) {
            if (nodes.length == 0) {
                throw new IllegalArgumentException("At least one node needs to be passed");
            }
            LevelNode rowNode = axisLevelNodeArgument(Axis.ROWS, nodes);
            LevelNode columnNode = axisLevelNodeArgument(Axis.COLUMNS, nodes);

            // the row node starting point
            BucketMap bucketMap;
            if (rowNode == null) {
                // if no row node was passed, start from the root
                bucketMap = bucketingService.getRowRootChildren();
            } else {
                bucketMap = rowNode.childrenMap;
            }

            // descend on row total nodes
            for (int idx = rowNode.axisDepth + 1; bucketMap != null
                    && idx < axisLevels.get(Axis.ROWS.ordinal()).size(); ++idx) {
                bucketMap = (BucketMap) bucketMap.getTotal();
            }

            Object bucketValue;
            if (bucketMap != null) {
                LinkedList<Bucket> columnBuckets = new LinkedList<Bucket>();
                if (columnNode != null) {
                    // add all column node parent buckets except the root
                    for (LevelNode node = columnNode; node.axisDepth > 0; node = node.parent) {
                        columnBuckets.addFirst(node.bucket);
                    }
                }

                // descend on the root column node
                bucketValue = bucketMap.get(axisRoots[Axis.COLUMNS.ordinal()]);
                // descend on regular column nodes
                Iterator<Bucket> columnBucketIterator = columnBuckets.iterator();
                for (int idx = 1; idx < axisLevels.get(Axis.COLUMNS.ordinal()).size(); ++idx) {
                    if (bucketValue == null) {
                        // the bucket combination does not exist
                        break;
                    }

                    bucketMap = (BucketMap) bucketValue;
                    if (columnBucketIterator.hasNext()) {
                        // if we have a bucket value, descend on the value
                        Bucket bucket = columnBucketIterator.next();
                        bucketValue = bucketMap.get(bucket);
                    } else {
                        // if there's no value, descend on totals
                        bucketValue = bucketMap.getTotal();
                    }
                }
            } else {
                bucketValue = null;
            }

            MeasureValue[] values;
            if (bucketValue == null) {
                // the bucket combination does not exist
                values = bucketingService.getZeroUserMeasureValues();
            } else {
                MeasureValue[] rawValues = (MeasureValue[]) bucketValue;
                values = bucketingService.getUserMeasureValues(rawValues);
            }

            List<net.sf.jasperreports.engine.analytics.data.MeasureValue> measureValues = new ArrayList<net.sf.jasperreports.engine.analytics.data.MeasureValue>(
                    values.length);
            for (int i = 0; i < values.length; i++) {
                Measure measure = measures.get(i);
                Object value = values[i].getValue();
                StandardMeasureValue measureValue = new StandardMeasureValue(measure, value);
                measureValues.add(measureValue);
            }

            if (log.isTraceEnabled()) {
                log.trace("values for (" + rowNode + ", " + columnNode + "): " + measureValues);
            }

            return measureValues;
        }

        protected LevelNode axisLevelNodeArgument(Axis axis, AxisLevelNode... nodes) {
            LevelNode axisNode = null;
            for (AxisLevelNode node : nodes) {
                if (node.getLevel().getAxis() == axis) {
                    if (axisNode != null) {
                        throw new IllegalArgumentException("More than one node on axis " + axis);
                    }

                    if (!(node instanceof LevelNode)) {
                        throw new IllegalArgumentException(
                                "The method should be called with nodes created by this data source");
                    }

                    axisNode = (LevelNode) node;
                }
            }
            return axisNode;
        }
    }

    protected class LevelNode implements AxisLevelNode {
        protected final Axis axis;
        protected final int axisDepth;
        protected final LevelNode parent;
        protected final Bucket bucket;
        protected final BucketMap childrenMap;

        protected Object value;
        protected String label;
        protected PropertyValues propertyValues;

        public LevelNode(Axis axis, int axisDepth, LevelNode parent, Bucket bucket, BucketMap childrenMap) {
            this.axis = axis;
            this.axisDepth = axisDepth;
            this.parent = parent;
            this.bucket = bucket;
            this.childrenMap = childrenMap;

            initValues();
        }

        private void initValues() {
            Object bucketValue = bucket.getValue();
            Object nodeValue = bucketValue;
            String label = null;
            PropertyValues propertyValues = null;

            if (axisDepth > 0 && bucketValue != null) {
                DataAxisLevel dataLevel = data.getDataAxis(axis).getLevels().get(axisDepth - 1);
                DataLevelBucket dataBucket = dataLevel.getBucket();
                List<DataLevelBucketProperty> bucketProperties = dataBucket.getBucketProperties();
                if (dataBucket.getLabelExpression() != null
                        || (bucketProperties != null && !bucketProperties.isEmpty())) {
                    // unwrap the raw value and the property values
                    ValuePropertiesWrapper valueWrapper = (ValuePropertiesWrapper) bucketValue;
                    nodeValue = valueWrapper.getValue();
                    label = valueWrapper.getLabel();

                    if (bucketProperties != null && !bucketProperties.isEmpty()) {
                        Map<String, Integer> propertyIndexes = axisLevelBucketPropertyIndexes.get(dataLevel);
                        Object[] propertyValuesArray = valueWrapper.getPropertyValues();
                        propertyValues = new MappedPropertyValues(propertyIndexes, propertyValuesArray);
                    }
                }
            }

            this.value = nodeValue;
            this.label = label;
            this.propertyValues = propertyValues;
        }

        @Override
        public AxisLevel getLevel() {
            return axisLevels.get(axis.ordinal()).get(axisDepth);
        }

        @Override
        public boolean isTotal() {
            return axisDepth == 0 // the root level is considered total
                    || bucket.isTotal();
        }

        @Override
        public Object getValue() {
            return value;
        }

        @Override
        public String getLabel() {
            return label;
        }

        @Override
        public PropertyValues getNodePropertyValues() {
            return propertyValues;
        }

        @Override
        public AxisLevelNode getParent() {
            return parent;
        }

        @Override
        public List<? extends AxisLevelNode> getChildren() {
            if (axisDepth + 1 >= axisLevels.get(axis.ordinal()).size()) {
                // last level on the axis
                return Collections.<AxisLevelNode>emptyList();
            }

            if (childrenMap == null) {
                // this happens for the root node when there was no data
                return Collections.<AxisLevelNode>emptyList();
            }

            List<LevelNode> children = new ArrayList<LevelNode>(childrenMap.size());
            for (Iterator<Entry<Bucket, Object>> entryIterator = childrenMap.entryIterator(); entryIterator
                    .hasNext();) {
                Entry<Bucket, Object> entry = entryIterator.next();
                Bucket childBucket = entry.getKey();
                Object entryValue = entry.getValue();
                BucketMap nextChildMap = entryValue instanceof BucketMap ? (BucketMap) entryValue : null;

                LevelNode childNode = new LevelNode(axis, axisDepth + 1, this, childBucket, nextChildMap);
                children.add(childNode);
            }

            if (log.isTraceEnabled()) {
                log.trace("children of " + this + ": " + children);
            }

            return children;
        }

        // TODO lucianc equals & hashcode

        @Override
        public String toString() {
            return bucket + " on " + axis + " level " + axisDepth;
        }
    }

    protected static class ServiceContext implements BucketingServiceContext {
        private final JasperReportsContext jasperReportsContext;
        private final JRFillExpressionEvaluator expressionEvaluator;

        public ServiceContext(JasperReportsContext jasperReportsContext,
                JRFillExpressionEvaluator expressionEvaluator) {
            this.jasperReportsContext = jasperReportsContext;
            this.expressionEvaluator = expressionEvaluator;
        }

        @Override
        public JasperReportsContext getJasperReportsContext() {
            return jasperReportsContext;
        }

        @Override
        public JRFillExpressionEvaluator getExpressionEvaluator() {
            return expressionEvaluator;
        }

        @Override
        public Object evaluateMeasuresExpression(JRExpression expression, MeasureValue[] measureValues)
                throws JRException {
            throw new UnsupportedOperationException();
        }

    }

    protected static class PropertiesWrapperBucketDefintion extends BucketDefinition {
        public PropertiesWrapperBucketDefintion(Comparator<Object> comparator, BucketOrder order,
                CrosstabTotalPositionEnum totalPosition) throws JRException {
            super(ValuePropertiesWrapper.class, null,
                    comparator == null ? null : new ValuePropertiesWrapperComparator(comparator), order,
                    totalPosition);
        }

        @Override
        public Bucket create(Object value) {
            if (value instanceof ValuePropertiesWrapper && ((ValuePropertiesWrapper) value).getValue() == null) {
                return new Bucket(value, VALUE_TYPE_NULL);
            }

            return super.create(value);
        }

    }
}