net.sf.jasperreports.engine.fill.JRFillCrosstab.java Source code

Java tutorial

Introduction

Here is the source code for net.sf.jasperreports.engine.fill.JRFillCrosstab.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.fill;

import java.awt.Color;
import java.util.ArrayList;
import java.util.Arrays;
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.ListIterator;
import java.util.Map;
import java.util.Map.Entry;
import java.util.NavigableMap;
import java.util.ResourceBundle;
import java.util.Set;
import java.util.TreeMap;
import java.util.concurrent.atomic.AtomicInteger;

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

import net.sf.jasperreports.annotations.properties.Property;
import net.sf.jasperreports.annotations.properties.PropertyScope;
import net.sf.jasperreports.components.iconlabel.IconLabelComponent;
import net.sf.jasperreports.components.iconlabel.IconLabelComponentUtil;
import net.sf.jasperreports.components.table.fill.BuiltinExpressionEvaluatorFactory;
import net.sf.jasperreports.components.table.fill.TableReport;
import net.sf.jasperreports.crosstabs.CrosstabColumnCell;
import net.sf.jasperreports.crosstabs.CrosstabDeepVisitor;
import net.sf.jasperreports.crosstabs.JRCellContents;
import net.sf.jasperreports.crosstabs.JRCrosstab;
import net.sf.jasperreports.crosstabs.JRCrosstabBucket;
import net.sf.jasperreports.crosstabs.JRCrosstabCell;
import net.sf.jasperreports.crosstabs.JRCrosstabColumnGroup;
import net.sf.jasperreports.crosstabs.JRCrosstabDataset;
import net.sf.jasperreports.crosstabs.JRCrosstabGroup;
import net.sf.jasperreports.crosstabs.JRCrosstabMeasure;
import net.sf.jasperreports.crosstabs.JRCrosstabParameter;
import net.sf.jasperreports.crosstabs.JRCrosstabRowGroup;
import net.sf.jasperreports.crosstabs.base.JRBaseCrosstab;
import net.sf.jasperreports.crosstabs.design.JRDesignCrosstab;
import net.sf.jasperreports.crosstabs.fill.BucketExpressionOrderer;
import net.sf.jasperreports.crosstabs.fill.BucketOrderer;
import net.sf.jasperreports.crosstabs.fill.IconLabelFillObjectFactory;
import net.sf.jasperreports.crosstabs.fill.JRCrosstabExpressionEvaluator;
import net.sf.jasperreports.crosstabs.fill.JRFillCrosstabCell;
import net.sf.jasperreports.crosstabs.fill.JRFillCrosstabColumnGroup;
import net.sf.jasperreports.crosstabs.fill.JRFillCrosstabGroup;
import net.sf.jasperreports.crosstabs.fill.JRFillCrosstabMeasure;
import net.sf.jasperreports.crosstabs.fill.JRFillCrosstabObjectFactory;
import net.sf.jasperreports.crosstabs.fill.JRFillCrosstabParameter;
import net.sf.jasperreports.crosstabs.fill.JRFillCrosstabRowGroup;
import net.sf.jasperreports.crosstabs.fill.calculation.BucketDefinition;
import net.sf.jasperreports.crosstabs.fill.calculation.BucketDefinition.Bucket;
import net.sf.jasperreports.crosstabs.fill.calculation.BucketingServiceContext;
import net.sf.jasperreports.crosstabs.fill.calculation.ColumnValueInfo;
import net.sf.jasperreports.crosstabs.fill.calculation.CrosstabBucketingService;
import net.sf.jasperreports.crosstabs.fill.calculation.CrosstabCell;
import net.sf.jasperreports.crosstabs.fill.calculation.HeaderCell;
import net.sf.jasperreports.crosstabs.fill.calculation.MeasureDefinition;
import net.sf.jasperreports.crosstabs.fill.calculation.MeasureDefinition.MeasureValue;
import net.sf.jasperreports.crosstabs.fill.calculation.OrderByColumnInfo;
import net.sf.jasperreports.crosstabs.fill.calculation.OrderByColumnOrderer;
import net.sf.jasperreports.crosstabs.interactive.CrosstabInteractiveJsonHandler;
import net.sf.jasperreports.crosstabs.interactive.DataColumnInfo;
import net.sf.jasperreports.crosstabs.interactive.RowGroupInteractiveInfo;
import net.sf.jasperreports.crosstabs.type.CrosstabColumnPositionEnum;
import net.sf.jasperreports.crosstabs.type.CrosstabPercentageEnum;
import net.sf.jasperreports.crosstabs.type.CrosstabRowPositionEnum;
import net.sf.jasperreports.engine.JRChild;
import net.sf.jasperreports.engine.JRElement;
import net.sf.jasperreports.engine.JRException;
import net.sf.jasperreports.engine.JRExpression;
import net.sf.jasperreports.engine.JRExpressionChunk;
import net.sf.jasperreports.engine.JRExpressionCollector;
import net.sf.jasperreports.engine.JRGenericElementType;
import net.sf.jasperreports.engine.JRLineBox;
import net.sf.jasperreports.engine.JROrigin;
import net.sf.jasperreports.engine.JRParameter;
import net.sf.jasperreports.engine.JRPrintElement;
import net.sf.jasperreports.engine.JRPrintFrame;
import net.sf.jasperreports.engine.JRPropertiesUtil;
import net.sf.jasperreports.engine.JRRuntimeException;
import net.sf.jasperreports.engine.JRStaticText;
import net.sf.jasperreports.engine.JRTextElement;
import net.sf.jasperreports.engine.JRTextField;
import net.sf.jasperreports.engine.JRVariable;
import net.sf.jasperreports.engine.JRVisitor;
import net.sf.jasperreports.engine.JasperCompileManager;
import net.sf.jasperreports.engine.JasperReport;
import net.sf.jasperreports.engine.JasperReportsContext;
import net.sf.jasperreports.engine.analytics.dataset.BucketOrder;
import net.sf.jasperreports.engine.design.JRDesignComponentElement;
import net.sf.jasperreports.engine.design.JRDesignTextField;
import net.sf.jasperreports.engine.export.HtmlExporter;
import net.sf.jasperreports.engine.export.MatcherExporterFilter;
import net.sf.jasperreports.engine.type.HorizontalPosition;
import net.sf.jasperreports.engine.type.ModeEnum;
import net.sf.jasperreports.engine.type.RunDirectionEnum;
import net.sf.jasperreports.engine.type.SortOrderEnum;
import net.sf.jasperreports.engine.util.ElementsVisitorUtils;
import net.sf.jasperreports.engine.util.JRValueStringUtils;
import net.sf.jasperreports.engine.xml.JRXmlConstants;
import net.sf.jasperreports.properties.PropertyConstants;
import net.sf.jasperreports.web.util.JacksonUtil;

/**
 * Fill-time implementation of a {@link net.sf.jasperreports.crosstabs.JRCrosstab crosstab}.
 * 
 * @author Lucian Chirita (lucianc@users.sourceforge.net)
 */
public class JRFillCrosstab extends JRFillElement implements JRCrosstab, JROriginProvider, BucketingServiceContext {
    private final static Log log = LogFactory.getLog(JRFillCrosstab.class);

    public static final String EXCEPTION_MESSAGE_KEY_BUCKETING_SERVICE_ERROR = "crosstabs.bucketing.service.error";
    public static final String EXCEPTION_MESSAGE_KEY_EVALUATOR_LOADING_ERROR = "crosstabs.evaluator.loading.error";
    public static final String EXCEPTION_MESSAGE_KEY_INFINITE_LOOP = "crosstabs.infinite.loop";
    public static final String EXCEPTION_MESSAGE_KEY_NOT_ENOUGH_SPACE = "crosstabs.not.enough.space";

    /**
     *   Property that enables/disables the interactivity in the crosstab component.
     *
     * <p>
     * It can be set:
     * <ul>
     *    <li>globally</li>
     *    <li>at report level</li>
     *    <li>at component level</li>
     * </ul>
     * </p>
     *
     * <p>
     * The default global value of this property is <code>true</code>
     * </p>
     */
    @Property(category = PropertyConstants.CATEGORY_CROSSTAB, defaultValue = PropertyConstants.BOOLEAN_TRUE, scopes = {
            PropertyScope.CONTEXT, PropertyScope.REPORT,
            PropertyScope.CROSSTAB }, sinceVersion = PropertyConstants.VERSION_5_5_0, valueType = Boolean.class)
    public static final String PROPERTY_INTERACTIVE = JRPropertiesUtil.PROPERTY_PREFIX + "crosstab.interactive";

    /**
     *   Property that enables/disables the floating headers in the crosstab component when scrolling.
     *   If the interactivity has been disabled by setting {@link #PROPERTY_INTERACTIVE} to <code>false</code>, then
     *   setting this property will have no effect.
     *
     * <p>
     * It can be set:
     * <ul>
     *    <li>globally</li>
     *    <li>at report level</li>
     *    <li>at component level</li>
     * </ul>
     * </p>
     *
     * <p>
     * The default global value of this property is <code>true</code>
     * </p>
     */
    @Property(category = PropertyConstants.CATEGORY_CROSSTAB, defaultValue = PropertyConstants.BOOLEAN_TRUE, scopes = {
            PropertyScope.CONTEXT, PropertyScope.REPORT,
            PropertyScope.CROSSTAB }, sinceVersion = PropertyConstants.VERSION_6_0_0, valueType = Boolean.class)
    public static final String PROPERTY_FLOATING_HEADERS = JRPropertiesUtil.PROPERTY_PREFIX
            + "crosstab.floating.headers";

    public static final String PROPERTY_ORDER_BY_COLUMN = JRPropertiesUtil.PROPERTY_PREFIX
            + "crosstab.order.by.column";

    @Property(category = PropertyConstants.CATEGORY_CROSSTAB, scopes = {
            PropertyScope.ELEMENT }, sinceVersion = PropertyConstants.VERSION_5_5_0, valueType = Integer.class)
    public static final String PROPERTY_ROW_GROUP_COLUMN_HEADER = JRPropertiesUtil.PROPERTY_PREFIX
            + "crosstab.row.group.column.header";

    @Property(category = PropertyConstants.CATEGORY_CROSSTAB, scopes = {
            PropertyScope.ELEMENT }, sinceVersion = PropertyConstants.VERSION_5_5_0, valueType = Integer.class)
    public static final String PROPERTY_COLUMN_HEADER_SORT_MEASURE_INDEX = JRPropertiesUtil.PROPERTY_PREFIX
            + "crosstab.column.header.sort.measure.index";

    public static final String CROSSTAB_INTERACTIVE_ELEMENT_NAME = "crosstabInteractiveElement";

    public static final String HTML_CLASS_COLUMN_FLOATING = "jrxtcolfloating";

    public static final String HTML_CLASS_ROW_FLOATING = "jrxtrowfloating";

    public static final String HTML_CLASS_CROSS_FLOATING = "jrxtcrossfloating";

    public static final JRGenericElementType CROSSTAB_INTERACTIVE_ELEMENT_TYPE = new JRGenericElementType(
            JRXmlConstants.JASPERREPORTS_NAMESPACE, CROSSTAB_INTERACTIVE_ELEMENT_NAME);

    protected static final String FILL_CACHE_KEY_CROSSTAB_CHUNK_COUNTER = JRFillCrosstab.class.getName()
            + "#chunkCounter";

    private final JRFillObjectFactory fillFactory;

    final protected JRCrosstab parentCrosstab;

    protected final JRLineBox lineBox;

    protected JRFillCrosstabDataset dataset;

    protected JRFillCrosstabRowGroup[] rowGroups;

    protected Map<String, Integer> rowGroupsMap;

    protected JRFillCrosstabColumnGroup[] columnGroups;

    protected Map<String, Integer> columnGroupsMap;

    protected JRFillCrosstabMeasure[] measures;

    private OrderByColumnInfo orderByColumnInfo;
    private List<Bucket> orderByColumnBucketValues;

    protected CrosstabBucketingService bucketingService;

    protected JRFillVariable[] variables;

    protected Map<String, JRFillVariable> variablesMap;

    protected JRFillVariable[][][] totalVariables;
    protected boolean[][] retrieveTotal;

    protected JRFillCrosstabParameter[] parameters;

    protected Map<String, JRFillParameter> parametersMap;

    protected boolean ignoreWidth;

    protected JRCrosstabExpressionEvaluator crosstabEvaluator;

    protected JRFillCrosstabCell[][] crossCells;
    protected JRFillCellContents titleCellContents;
    protected JRFillCellContents headerCell;
    protected JRFillCellContents whenNoDataCell;

    protected boolean hasData;
    protected HeaderCell[][] columnHeadersData;
    protected HeaderCell[][] rowHeadersData;
    protected CrosstabCell[][] cellData;
    protected MeasureValue[] grandTotals;

    private boolean percentage;

    private CrosstabFiller crosstabFiller;
    private int overflowStartPage;

    private List<JRTemplatePrintFrame> printFrames;

    private boolean interactive;
    private boolean floatingHeaders;
    private int lastColumnGroupWithHeaderIndex = -1;

    public JRFillCrosstab(JRBaseFiller filler, JRCrosstab crosstab, JRFillObjectFactory factory) {
        super(filler, crosstab, factory);

        this.fillFactory = factory;

        parentCrosstab = crosstab;
        lineBox = crosstab.getLineBox().clone(this);

        loadEvaluator(filler.getJasperReport());

        JRFillCrosstabObjectFactory crosstabFactory = new JRFillCrosstabObjectFactory(factory, crosstabEvaluator);
        crosstabFactory.setParentOriginProvider(this);

        if (crosstab.getTitleCell() != null && crosstab.getTitleCell().getCellContents() != null) {
            titleCellContents = crosstabFactory.getCell(crosstab.getTitleCell().getCellContents(),
                    JRCellContents.TYPE_CROSSTAB_TITLE);
        }

        headerCell = crosstabFactory.getCell(crosstab.getHeaderCell(), JRCellContents.TYPE_CROSSTAB_HEADER);

        copyRowGroups(crosstab, crosstabFactory);
        copyColumnGroups(crosstab, crosstabFactory);

        copyMeasures(crosstab, crosstabFactory);
        copyCells(crosstab, crosstabFactory);
        whenNoDataCell = crosstabFactory.getCell(crosstab.getWhenNoDataCell(), JRCellContents.TYPE_NO_DATA_CELL);

        dataset = factory.getCrosstabDataset(crosstab.getDataset(), this);
        crosstabEvaluator.setFillDataset(dataset.getFillDataset());

        copyParameters(crosstab, factory);
        copyVariables(crosstab, crosstabFactory);

        lastColumnGroupWithHeaderIndex = determineLastColumnGroupWithHeaderIndex();

        crosstabFiller = new CrosstabFiller();
    }

    @Override
    protected void setBand(JRFillBand band) {
        super.setBand(band);

        dataset.setBand(band);
    }

    private boolean isIgnoreWidth(JRBaseFiller filler, JRCrosstab crosstab) {
        Boolean crosstabIgnoreWidth = crosstab.getIgnoreWidth();
        // crosstab attribute overrides all 
        if (crosstabIgnoreWidth != null) {
            return crosstabIgnoreWidth;
        }

        // report level property
        String reportProperty = JRPropertiesUtil.getOwnProperty(filler.getMainDataset(), PROPERTY_IGNORE_WIDTH);
        if (reportProperty != null) {
            return JRPropertiesUtil.asBoolean(reportProperty);
        }

        // report pagination flag from the filler
        if (filler.isIgnorePagination()) {
            return true;
        }

        // global property
        return filler.getPropertiesUtil().getBooleanProperty(PROPERTY_IGNORE_WIDTH);
    }

    @Override
    public ModeEnum getModeValue() {
        return getStyleResolver().getMode(this, ModeEnum.TRANSPARENT);
    }

    private void copyRowGroups(JRCrosstab crosstab, JRFillCrosstabObjectFactory factory) {
        JRCrosstabRowGroup[] groups = crosstab.getRowGroups();
        rowGroups = new JRFillCrosstabRowGroup[groups.length];
        rowGroupsMap = new HashMap<String, Integer>();
        for (int i = 0; i < groups.length; ++i) {
            JRFillCrosstabRowGroup group = factory.getCrosstabRowGroup(groups[i]);
            group.getFillHeader().setVerticalPositionType(groups[i].getPositionValue());

            rowGroups[i] = group;
            rowGroupsMap.put(group.getName(), i);
        }
    }

    private void copyColumnGroups(JRCrosstab crosstab, JRFillCrosstabObjectFactory factory) {
        JRCrosstabColumnGroup[] groups = crosstab.getColumnGroups();
        columnGroups = new JRFillCrosstabColumnGroup[groups.length];
        columnGroupsMap = new HashMap<String, Integer>();
        for (int i = 0; i < groups.length; ++i) {
            JRFillCrosstabColumnGroup group = factory.getCrosstabColumnGroup(groups[i]);
            columnGroups[i] = group;
            columnGroupsMap.put(group.getName(), i);
        }
    }

    private void copyMeasures(JRCrosstab crosstab, JRFillCrosstabObjectFactory factory) {
        JRCrosstabMeasure[] crossMeasures = crosstab.getMeasures();
        measures = new JRFillCrosstabMeasure[crossMeasures.length];
        for (int i = 0; i < crossMeasures.length; i++) {
            measures[i] = factory.getCrosstabMeasure(crossMeasures[i]);
        }
    }

    private void copyParameters(JRCrosstab crosstab, JRFillObjectFactory factory) {
        JRCrosstabParameter[] crossParams = crosstab.getParameters();
        parameters = new JRFillCrosstabParameter[crossParams.length];
        parametersMap = new HashMap<String, JRFillParameter>();
        for (int i = 0; i < crossParams.length; i++) {
            parameters[i] = factory.getCrosstabParameter(crossParams[i]);
            parametersMap.put(parameters[i].getName(), parameters[i]);
        }
    }

    private void copyCells(JRCrosstab crosstab, JRFillCrosstabObjectFactory factory) {
        JRCrosstabCell[][] crosstabCells = crosstab.getCells();
        crossCells = new JRFillCrosstabCell[rowGroups.length + 1][columnGroups.length + 1];
        for (int i = 0; i <= rowGroups.length; ++i) {
            for (int j = 0; j <= columnGroups.length; ++j) {
                if (crosstabCells[i][j] != null) {
                    crossCells[i][j] = factory.getCrosstabCell(crosstabCells[i][j]);
                }
            }
        }
    }

    private void copyVariables(JRCrosstab crosstab, JRFillObjectFactory factory) {
        JRVariable[] vars = crosstab.getVariables();
        variables = new JRFillVariable[vars.length];
        variablesMap = new HashMap<String, JRFillVariable>();
        for (int i = 0; i < variables.length; i++) {
            variables[i] = factory.getVariable(vars[i]);
            variablesMap.put(variables[i].getName(), variables[i]);
        }

        Map<String, int[]> totalVarPos = new HashMap<String, int[]>();
        totalVariables = new JRFillVariable[rowGroups.length + 1][columnGroups.length + 1][measures.length];
        for (int row = 0; row <= rowGroups.length; ++row) {
            JRCrosstabRowGroup rowGroup = row == rowGroups.length ? null : rowGroups[row];
            for (int col = 0; col <= columnGroups.length; ++col) {
                JRCrosstabColumnGroup colGroup = col == columnGroups.length ? null : columnGroups[col];

                if (row < rowGroups.length || col < columnGroups.length) {
                    for (int m = 0; m < measures.length; m++) {
                        String totalVariableName = JRDesignCrosstab.getTotalVariableName(measures[m], rowGroup,
                                colGroup);
                        totalVariables[row][col][m] = variablesMap.get(totalVariableName);
                        totalVarPos.put(totalVariableName, new int[] { row, col });
                    }
                }
            }
        }

        Set<String> measureVars = new HashSet<String>();
        for (JRFillCrosstabMeasure measure : measures) {
            measureVars.add(measure.getFillVariable().getName());
        }

        retrieveTotal = new boolean[rowGroups.length + 1][columnGroups.length + 1];

        //FIXME avoid this
        JRExpressionCollector collector = JRExpressionCollector.collector(filler.getJasperReportsContext(),
                filler.getJasperReport(), crosstab);
        List<JRExpression> expressions = collector.getExpressions(crosstab);
        for (Iterator<JRExpression> iter = expressions.iterator(); iter.hasNext();) {
            JRExpression expression = iter.next();
            Object expressionContext = collector.getExpressionContext(expression);
            boolean groupHeaderExpression = expressionContext instanceof JRCrosstabGroup;
            JRExpressionChunk[] chunks = expression.getChunks();
            if (chunks != null) {
                for (int i = 0; i < chunks.length; i++) {
                    JRExpressionChunk chunk = chunks[i];
                    if (chunk.getType() == JRExpressionChunk.TYPE_VARIABLE) {
                        String varName = chunk.getText();
                        int[] pos = totalVarPos.get(varName);
                        if (pos != null) {
                            retrieveTotal[pos[0]][pos[1]] = true;
                        }

                        // if a measure variable is used inside a group header, compute all totals.
                        // in theory we could have a finer grained rule here, but it complicates
                        // the logic without a singnificant gain.
                        if (groupHeaderExpression && (pos != null || measureVars.contains(varName))) {
                            retrieveTotal[0][0] = true;
                        }
                    }
                }
            }
        }
    }

    @Override
    public JRBaseFiller getFiller() {
        return filler;
    }

    protected void loadEvaluator(JasperReport jasperReport) {
        try {
            JREvaluator evaluator = JasperCompileManager.getInstance(filler.getJasperReportsContext())
                    .getEvaluator(jasperReport, parentCrosstab);
            crosstabEvaluator = new JRCrosstabExpressionEvaluator(evaluator);
        } catch (JRException e) {
            throw new JRRuntimeException(EXCEPTION_MESSAGE_KEY_EVALUATOR_LOADING_ERROR, (Object[]) null, e);
        }
    }

    private CrosstabBucketingService createService(byte evaluation) throws JRException {
        boolean hasOrderByExpression = false;
        List<BucketDefinition> rowBuckets = new ArrayList<BucketDefinition>(rowGroups.length);
        for (int i = 0; i < rowGroups.length; ++i) {
            JRFillCrosstabRowGroup group = rowGroups[i];
            rowBuckets.add(createServiceBucket(group, i, evaluation));
            hasOrderByExpression |= group.getBucket().getOrderByExpression() != null;
        }

        List<BucketDefinition> colBuckets = new ArrayList<BucketDefinition>(columnGroups.length);
        for (int i = 0; i < columnGroups.length; ++i) {
            JRFillCrosstabColumnGroup group = columnGroups[i];
            colBuckets.add(createServiceBucket(group, i, evaluation));
            hasOrderByExpression |= group.getBucket().getOrderByExpression() != null;
        }

        percentage = false;
        List<MeasureDefinition> measureList = new ArrayList<MeasureDefinition>(measures.length);
        for (int i = 0; i < measures.length; ++i) {
            measureList.add(createServiceMeasure(measures[i]));
            percentage |= measures[i].getPercentageType() == CrosstabPercentageEnum.GRAND_TOTAL;
        }

        // if a group has order by expression, compute totals as they might be used
        // in the expression
        // TODO refine this
        if (percentage || hasOrderByExpression) {
            rowBuckets.get(0).setComputeTotal();
            colBuckets.get(0).setComputeTotal();
        }

        return new CrosstabBucketingService(this, rowBuckets, colBuckets, measureList, dataset.isDataPreSorted(),
                retrieveTotal);
    }

    private BucketDefinition createServiceBucket(JRCrosstabGroup group, int groupIndex, byte evaluation)
            throws JRException {
        JRCrosstabBucket bucket = group.getBucket();

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

        BucketOrderer orderer = createOrderer(group, groupIndex, comparator);
        BucketDefinition bucketDefinition = new BucketDefinition(bucket.getValueClass(), orderer, comparator,
                bucket.getOrder(), group.getTotalPositionValue());

        Boolean mergeHeaderCells = group.getMergeHeaderCells();
        // by default the header cells are merged
        bucketDefinition.setMergeHeaderCells(mergeHeaderCells == null || mergeHeaderCells);

        return bucketDefinition;
    }

    protected BucketOrderer createOrderer(JRCrosstabGroup group, int groupIndex,
            Comparator<Object> bucketComparator) {
        BucketOrderer orderer = null;

        if (group instanceof JRCrosstabRowGroup && orderByColumnInfo != null && orderByColumnInfo.getOrder() != null
        // ordering by column only applies to nesting groups is they are not already ordered
                && (groupIndex == rowGroups.length - 1 || group.getBucket().getOrder() == BucketOrder.NONE)) {
            orderer = new OrderByColumnOrderer(orderByColumnInfo);
        }

        if (orderer == null) {
            JRCrosstabBucket bucket = group.getBucket();
            JRExpression orderByExpression = bucket.getOrderByExpression();
            if (orderByExpression != null && bucket.getOrder() != BucketOrder.NONE) {
                if (log.isDebugEnabled()) {
                    log.debug("using order by expression to order group " + group.getName());
                }

                // when we have an order by expression, the comparator is applied to order values
                Comparator<Object> orderValueComparator = BucketDefinition.createOrderComparator(bucketComparator,
                        bucket.getOrder());
                orderer = new BucketExpressionOrderer(orderByExpression, orderValueComparator);
            }
        }

        return orderer;
    }

    private MeasureDefinition createServiceMeasure(JRFillCrosstabMeasure measure) {
        return new MeasureDefinition(measure.getValueClass(), measure.getCalculationValue(),
                measure.getIncrementerFactory());
    }

    @Override
    public Object evaluateMeasuresExpression(JRExpression expression, MeasureValue[] measureValues)
            throws JRException {
        for (int i = 0; i < measures.length; i++) {
            Object value = measureValues[i].getValue();
            measures[i].getFillVariable().setValue(value);
        }

        return crosstabEvaluator.evaluate(expression, JRExpression.EVALUATION_DEFAULT);
    }

    @Override
    protected void reset() {
        super.reset();

        for (int i = 0; i < variables.length; i++) {
            variables[i].setValue(null);
            variables[i].setInitialized(true);
        }

        printFrames = null;
    }

    @Override
    protected void evaluate(byte evaluation) throws JRException {
        reset();

        evaluatePrintWhenExpression(evaluation);

        if (isPrintWhenExpressionNull() || isPrintWhenTrue()) {
            evaluateProperties(evaluation);
            evaluateStyle(evaluation);

            dataset.evaluateDatasetRun(evaluation);

            initEvaluator(evaluation);

            bucketingService.processData();
            bucketingService.createCrosstab();

            hasData = bucketingService.hasData();

            if (hasData) {
                columnHeadersData = bucketingService.getColumnHeaders();
                rowHeadersData = bucketingService.getRowHeaders();
                cellData = bucketingService.getCrosstabCells();
                if (percentage) {
                    grandTotals = bucketingService.getGrandTotals();
                }

                crosstabFiller.initCrosstab();
            }

            overflowStartPage = 0;

            ignoreWidth = isIgnoreWidth(filler, parentCrosstab);

            interactive = filler.getPropertiesUtil().getBooleanProperty(this, PROPERTY_INTERACTIVE, true);

            floatingHeaders = filler.getPropertiesUtil().getBooleanProperty(this, PROPERTY_FLOATING_HEADERS, true);
        }
    }

    protected void initEvaluator(byte evaluation) throws JRException {
        Map<String, Object> parameterValues = JRFillSubreport.getParameterValues(filler,
                getParametersMapExpression(), getParameters(), evaluation, true, false, //hasResourceBundle
                false//hasFormatFactory
        );

        ResourceBundle resBdl = (ResourceBundle) parameterValues.get(JRParameter.REPORT_RESOURCE_BUNDLE);
        if (resBdl == null) {
            JRFillParameter resourceBundleParam = filler.getParametersMap().get(JRParameter.REPORT_RESOURCE_BUNDLE);
            parameterValues.put(JRParameter.REPORT_RESOURCE_BUNDLE, resourceBundleParam.getValue());
        }

        parameterValues.put(JRParameter.REPORT_PARAMETERS_MAP, parameterValues);

        for (int i = 0; i < parameters.length; i++) {
            Object value = parameterValues.get(parameters[i].getName());
            parameters[i].setValue(value);
        }

        boolean ignoreNPE = filler.getPropertiesUtil().getBooleanProperty(this, JREvaluator.PROPERTY_IGNORE_NPE,
                true);
        crosstabEvaluator.init(parametersMap, variablesMap, filler.getWhenResourceMissingType(), ignoreNPE);
    }

    protected void initBucketingService() {
        if (bucketingService == null) {
            setOrderByColumnInfo();

            try {
                bucketingService = createService(JRExpression.EVALUATION_DEFAULT);
            } catch (JRException e) {
                throw new JRRuntimeException(EXCEPTION_MESSAGE_KEY_BUCKETING_SERVICE_ERROR, (Object[]) null, e);
            }

            setOrderByColumnBucketValues();
        } else {
            bucketingService.clear();
        }
    }

    protected void setOrderByColumnInfo() {
        orderByColumnInfo = null;

        // should we read this from evaluated properties?
        String orderByProperty = parentCrosstab.getPropertiesMap().getProperty(PROPERTY_ORDER_BY_COLUMN);
        if (orderByProperty == null || orderByProperty.isEmpty()) {
            return;
        }

        orderByColumnInfo = JacksonUtil.getInstance(filler.getJasperReportsContext()).loadObject(orderByProperty,
                OrderByColumnInfo.class);
    }

    protected void setOrderByColumnBucketValues() {
        if (orderByColumnInfo != null && orderByColumnInfo.getOrder() != null) {
            // creating an orderer for convenience
            OrderByColumnOrderer orderer = new OrderByColumnOrderer(orderByColumnInfo);
            orderer.init(bucketingService);
            orderByColumnBucketValues = orderer.getBucketValues();
        }
    }

    @Override
    protected boolean prepare(int availableHeight, boolean isOverflow) throws JRException {
        super.prepare(availableHeight, isOverflow);

        if (!isToPrint()) {
            return false;
        }

        if (availableHeight < getRelativeY() + getHeight()) {
            setToPrint(false);
            return true;
        }

        if (isOverflow && crosstabFiller.ended() && isAlreadyPrinted()) {
            if (isPrintWhenDetailOverflows()) {
                rewind();
                setReprinted(true);
            } else {
                setPrepareHeight(getHeight());
                setToPrint(false);

                return false;
            }
        }

        if (isOverflow && isPrintWhenDetailOverflows()) {
            setReprinted(true);
        }

        printFrames = new ArrayList<JRTemplatePrintFrame>();
        crosstabFiller.fill(availableHeight - getRelativeY());

        if (!printFrames.isEmpty()) {
            // crosstab content has been filled, reset overflowPage
            overflowStartPage = 0;
        } else {
            int pageCount = filler.getCurrentPageCount();
            if (overflowStartPage == 0) {
                // first empty page
                overflowStartPage = pageCount;
            } else if (pageCount >= overflowStartPage + 2) {
                throw new JRRuntimeException(EXCEPTION_MESSAGE_KEY_INFINITE_LOOP, (Object[]) null);
            }
        }

        boolean willOverflow = crosstabFiller.willOverflow();
        if (willOverflow) {
            setPrepareHeight(availableHeight - getRelativeY());
        } else if (!printFrames.isEmpty()) {
            JRTemplatePrintFrame lastFrame = printFrames.get(printFrames.size() - 1);
            int usedHeight = lastFrame.getY() + lastFrame.getHeight();
            setPrepareHeight(usedHeight);
        }

        return willOverflow;
    }

    protected void addCrosstabChunk(List<JRPrintElement> elements, int yOffset) {
        JRTemplatePrintFrame printFrame = new JRTemplatePrintFrame(getTemplateFrame(), printElementOriginator);
        //printFrame.setUUID(getUUID());
        printFrame.setX(0);
        printFrame.setY(yOffset);

        Collections.sort(elements, new JRYXComparator());//FIXME make singleton comparator; same for older comparator

        int xLimit = Integer.MIN_VALUE;
        int yLimit = Integer.MIN_VALUE;
        for (Iterator<JRPrintElement> it = elements.iterator(); it.hasNext();) {
            JRPrintElement element = it.next();
            if (element.getX() + element.getWidth() > xLimit) {
                xLimit = element.getX() + element.getWidth();
            }
            if (element.getY() + element.getHeight() > yLimit) {
                yLimit = element.getY() + element.getHeight();
            }
        }

        JRLineBox lineBox = getLineBox();
        int width = xLimit + lineBox.getLeftPadding() + lineBox.getRightPadding();
        printFrame.setWidth(width);

        HorizontalPosition position = concreteHorizontalPosition();
        switch (position) {
        case RIGHT:
            // the position does not apply when the crosstab is bigger than the element (ignoreWidth is set)
            // still, it applies if the crosstab is RTL
            if (width < getWidth() || getRunDirectionValue() == RunDirectionEnum.RTL) {
                // move to the right
                printFrame.setX(getWidth() - width);
            }
            break;
        case CENTER:
            // the position does not apply when the crosstab is bigger than the element (ignoreWidth is set)
            if (width < getWidth()) {
                int centeredX = (getWidth() - width) / 2;
                printFrame.setX(centeredX);
            }
            break;
        case LEFT:
        default:
            // x = 0 already set
            break;
        }

        int height = yLimit + lineBox.getTopPadding() + lineBox.getBottomPadding();
        printFrame.setHeight(height);

        if (getRunDirectionValue() == RunDirectionEnum.RTL) {
            mirrorPrintElements(elements, xLimit);
        }

        int chunkIndex = getChunkIndex();
        String chunkId = getUUID().toString() + "." + chunkIndex;

        if (interactive) {
            printFrame.getPropertiesMap().setProperty(CrosstabInteractiveJsonHandler.PROPERTY_CROSSTAB_ID, chunkId);

            JRTemplateGenericPrintElement genericElement = createInteractiveElement(chunkId, floatingHeaders);
            printFrame.addElement(genericElement);
        }

        // dump all elements into the print frame
        printFrame.addElements(elements);

        // add this frame to the list to the list of crosstab chunks
        printFrames.add(printFrame);
    }

    protected int getChunkIndex() {
        JRFillContext fillerContext = filler.getFillContext();
        AtomicInteger counter = (AtomicInteger) fillerContext.getFillCache(FILL_CACHE_KEY_CROSSTAB_CHUNK_COUNTER);
        if (counter == null) {
            // we just need a mutable integer, there's no actual concurrency here
            counter = new AtomicInteger();
            fillerContext.setFillCache(FILL_CACHE_KEY_CROSSTAB_CHUNK_COUNTER, counter);
        }

        int chunkIndex = counter.getAndIncrement();
        return chunkIndex;
    }

    protected HorizontalPosition concreteHorizontalPosition() {
        HorizontalPosition position = getHorizontalPosition();
        if (position == null) {
            position = getRunDirectionValue() == RunDirectionEnum.RTL ? HorizontalPosition.RIGHT
                    : HorizontalPosition.LEFT;
        }
        return position;
    }

    protected JRTemplateGenericPrintElement createInteractiveElement(String chunkId, boolean floatingHeaders) {
        // TODO lucianc cache
        JRTemplateGenericElement genericElementTemplate = new JRTemplateGenericElement(getElementOrigin(),
                defaultStyleProvider, CROSSTAB_INTERACTIVE_ELEMENT_TYPE);
        JRTemplateGenericPrintElement genericElement = new JRTemplateGenericPrintElement(genericElementTemplate,
                printElementOriginator);//use a different source id?
        genericElement.setX(0);
        genericElement.setY(0);
        genericElement.setWidth(1);
        genericElement.setHeight(1);

        genericElement.setParameterValue(CrosstabInteractiveJsonHandler.ELEMENT_PARAMETER_CROSSTAB_ID,
                getUUID().toString());
        genericElement.setParameterValue(CrosstabInteractiveJsonHandler.ELEMENT_PARAMETER_CROSSTAB_FRAGMENT_ID,
                chunkId);
        genericElement.setParameterValue(CrosstabInteractiveJsonHandler.ELEMENT_PARAMETER_START_COLUMN_INDEX,
                crosstabFiller.startColumnIndex);
        genericElement.setParameterValue(CrosstabInteractiveJsonHandler.ELEMENT_PARAMETER_FLOATING_HEADERS,
                floatingHeaders);

        BucketDefinition[] rowBuckets = bucketingService.getRowBuckets();
        List<RowGroupInteractiveInfo> rowGroups = new ArrayList<RowGroupInteractiveInfo>(rowBuckets.length);
        for (BucketDefinition bucket : rowBuckets) {
            RowGroupInteractiveInfo groupInfo = new RowGroupInteractiveInfo();
            groupInfo.setSortable(true);// TODO lucianc
            BucketOrder order = bucketOrder(bucket);
            groupInfo.setOrder(order);
            rowGroups.add(groupInfo);
        }
        genericElement.setParameterValue(CrosstabInteractiveJsonHandler.ELEMENT_PARAMETER_ROW_GROUPS, rowGroups);

        int dataColumnCount = crosstabFiller.lastColumnIndex - crosstabFiller.startColumnIndex;
        List<DataColumnInfo> dataColumns = new ArrayList<DataColumnInfo>(dataColumnCount);
        for (int colIdx = crosstabFiller.startColumnIndex; colIdx < crosstabFiller.lastColumnIndex; ++colIdx) {
            List<Bucket> bucketValues = null;
            // getting the values from the most detailed column header
            for (int level = columnHeadersData.length - 1; level >= 0 && bucketValues == null; --level) {
                HeaderCell header = columnHeadersData[level][colIdx];
                bucketValues = header == null ? null : bucketValuesList(header);
            }

            DataColumnInfo dataColumn = new DataColumnInfo();
            int sortMeasureIndex = measures.length <= 1 ? 0
                    : (crosstabFiller.dataColumnSortMeasures == null ? 0
                            : crosstabFiller.dataColumnSortMeasures[colIdx - crosstabFiller.startColumnIndex]);
            if (sortMeasureIndex >= 0) {
                // the column is sortable
                // TODO lucianc do not repeat this if not necessary + do not serialize nulls
                dataColumn.setSortMeasureIndex(sortMeasureIndex);

                List<ColumnValueInfo> columnValues = toColumnValues(bucketValues);
                dataColumn.setColumnValues(columnValues);
            }

            BucketOrder columnOrder = null;
            if (orderByColumnBucketValues != null && orderByColumnBucketValues.equals(bucketValues)) {
                columnOrder = BucketOrder.fromSortOrderEnum(orderByColumnInfo.getOrder());
            }
            dataColumn.setOrder(columnOrder);

            dataColumns.add(dataColumn);
        }
        genericElement.setParameterValue(CrosstabInteractiveJsonHandler.ELEMENT_PARAMETER_DATA_COLUMNS,
                dataColumns);

        return genericElement;
    }

    protected BucketOrder bucketOrder(BucketDefinition bucket) {
        return (dataset.isDataPreSorted() || bucket.getOrderer() != null) ? BucketOrder.NONE : bucket.getOrder();
    }

    protected boolean matchesOrderByColumn(HeaderCell cell) {
        if (orderByColumnBucketValues == null) {
            return false;
        }

        List<Bucket> cellValues = bucketValuesList(cell);
        if (cellValues.size() > orderByColumnBucketValues.size()) {
            return false;
        }

        // when the last column group is empty, we will only match a part of orderByColumnBucketValues
        Iterator<Bucket> orderValueIt = orderByColumnBucketValues.iterator();
        for (Bucket cellValue : cellValues) {
            Bucket orderValue = orderValueIt.next();
            if (!cellValue.equals(orderValue)) {
                return false;
            }
        }
        return true;
    }

    protected List<Bucket> bucketValuesList(HeaderCell cell) {
        Bucket[] values = cell.getBucketValues();
        ArrayList<Bucket> valuesList = new ArrayList<Bucket>(values.length);
        for (Bucket bucket : values) {
            if (bucket != null) {
                valuesList.add(bucket);
            }
        }
        return valuesList;
    }

    protected List<ColumnValueInfo> toColumnValues(List<Bucket> bucketValues) {
        List<ColumnValueInfo> columnValues = new ArrayList<ColumnValueInfo>(bucketValues.size());
        for (Bucket bucket : bucketValues) {
            if (bucket != null) {
                ColumnValueInfo columnValue = new ColumnValueInfo();
                if (bucket.isTotal()) {
                    columnValue.setTotal(true);
                } else {
                    Object value = bucket.getValue();
                    if (value != null) {
                        String valueType = value.getClass().getName();
                        columnValue.setValueType(valueType);
                        columnValue.setValue(JRValueStringUtils.serialize(valueType, value));
                    }
                }

                columnValues.add(columnValue);
            }
        }
        return columnValues;
    }

    @Override
    protected JRPrintElement fill() {
        // don't return anything, see getPrintElements()
        return null;
    }

    protected JRTemplateFrame getTemplateFrame() {
        return (JRTemplateFrame) getElementTemplate();
    }

    @Override
    protected JRTemplateElement createElementTemplate() {
        JRTemplateFrame template = new JRTemplateFrame(getElementOrigin(),
                filler.getJasperPrint().getDefaultStyleProvider());
        template.setElement(this);
        template.copyBox(getLineBox());
        return template;
    }

    @Override
    protected void rewind() {
        crosstabFiller.initCrosstab();

        overflowStartPage = 0;
    }

    protected List<? extends JRPrintElement> getPrintElements() {
        return printFrames;
    }

    protected void mirrorPrintElements(List<JRPrintElement> printElements, int width) {
        for (Iterator<JRPrintElement> it = printElements.iterator(); it.hasNext();) {
            JRPrintElement element = it.next();
            int mirrorX = width - element.getX() - element.getWidth();
            element.setX(mirrorX);
        }
    }

    @Override
    protected void resolveElement(JRPrintElement element, byte evaluation) {
        // nothing
    }

    @Override
    public void collectExpressions(JRExpressionCollector collector) {
        collector.collect(this);
    }

    @Override
    public void visit(JRVisitor visitor) {
        visitor.visitCrosstab(this);

        if (ElementsVisitorUtils.visitDeepElements(visitor)) {
            new CrosstabDeepVisitor(visitor).deepVisitCrosstab(this);
        }
    }

    @Override
    public int getId() {
        return parentCrosstab.getId();
    }

    @Override
    public JRCrosstabDataset getDataset() {
        return dataset;
    }

    @Override
    public JRCrosstabRowGroup[] getRowGroups() {
        return rowGroups;
    }

    @Override
    public JRCrosstabColumnGroup[] getColumnGroups() {
        return columnGroups;
    }

    @Override
    public JRCrosstabMeasure[] getMeasures() {
        return measures;
    }

    private int determineLastColumnGroupWithHeaderIndex() {
        int colGroupIdx = columnGroups.length - 1;
        while (colGroupIdx >= 0) {
            JRCellContents header = columnGroups[colGroupIdx].getHeader();
            if (header != null && !header.getChildren().isEmpty()) {
                break;
            }

            --colGroupIdx;
        }

        int lastGroupIndex = colGroupIdx >= 0 ? colGroupIdx : 0;
        if (log.isDebugEnabled()) {
            log.debug("last column group with header is " + lastGroupIndex);
        }
        return lastGroupIndex;
    }

    /**
     * Fill-time crosstab input dataset implementation.
     *  
     * @author Lucian Chirita (lucianc@users.sourceforge.net)
     */
    public class JRFillCrosstabDataset extends JRFillElementDataset implements JRCrosstabDataset {
        public static final String EXCEPTION_MESSAGE_KEY_DATASET_INCREMENTING_ERROR = "crosstabs.dataset.incrementing.error";

        private Object[] bucketValues;

        private Object[] measureValues;

        public JRFillCrosstabDataset(JRCrosstabDataset dataset, JRFillObjectFactory factory) {
            super(dataset, factory);

            this.bucketValues = new Object[rowGroups.length + columnGroups.length];
            this.measureValues = new Object[measures.length];
        }

        @Override
        protected void customInitialize() {
            initBucketingService();
        }

        @Override
        protected void customEvaluate(JRCalculator calculator) throws JRExpressionEvalException {
            for (int i = 0; i < rowGroups.length; i++) {
                bucketValues[i] = calculator.evaluate(rowGroups[i].getBucket().getExpression());
            }

            for (int i = 0; i < columnGroups.length; ++i) {
                bucketValues[i + rowGroups.length] = calculator
                        .evaluate(columnGroups[i].getBucket().getExpression());
            }

            for (int i = 0; i < measures.length; i++) {
                measureValues[i] = calculator.evaluate(measures[i].getValueExpression());
            }
        }

        @Override
        protected void customIncrement() {
            try {
                bucketingService.addData(bucketValues, measureValues);
            } catch (JRException e) {
                throw new JRRuntimeException(EXCEPTION_MESSAGE_KEY_DATASET_INCREMENTING_ERROR, (Object[]) null, e);
            }
        }

        @Override
        public void collectExpressions(JRExpressionCollector collector) {
        }

        @Override
        public boolean isDataPreSorted() {
            return ((JRCrosstabDataset) parent).isDataPreSorted();
        }
    }

    /**
     * Crosstab filler class.
     *  
     * @author Lucian Chirita (lucianc@users.sourceforge.net)
     */
    protected class CrosstabFiller {
        private int yOffset;
        private int yChunkOffset;
        private boolean willOverflow;

        private int[] rowHeadersXOffsets;

        private boolean[] columnBreakable;
        private boolean[] rowBreakable;
        private int[] columnCount;
        private int[] rowCount;
        private int[] columnXOffsets;

        private boolean noDataCellPrinted;
        private boolean titlePrinted;

        private int startRowIndex;
        private int startColumnIndex;
        private int lastColumnIndex;
        private List<HeaderCell[]> columnHeaders;

        private List<List<JRPrintElement>> printRows;
        private int[] dataColumnSortMeasures;

        private HeaderCell[] spanHeaders;
        private int[] spanHeadersStart;

        private List<Integer> rowYs = new ArrayList<Integer>();
        private int rowIdx;

        private List<JRFillCellContents> preparedRow = new ArrayList<JRFillCellContents>();
        private int preparedRowHeight;

        private boolean printRowHeaders;
        private boolean printColumnHeaders;

        private JRFillVariable rowCountVar;
        private JRFillVariable colCountVar;

        protected CrosstabFiller() {
            setRowHeadersXOffsets();

            printRows = new ArrayList<List<JRPrintElement>>();

            rowCountVar = variablesMap.get(JRCrosstab.VARIABLE_ROW_COUNT);
            colCountVar = variablesMap.get(JRCrosstab.VARIABLE_COLUMN_COUNT);
        }

        protected void initCrosstab() {
            columnXOffsets = computeOffsets(columnHeadersData, columnGroups, true);
            columnBreakable = computeBreakableHeaders(columnHeadersData, columnGroups, columnXOffsets, true, true);
            columnCount = computeCounts(columnHeadersData);

            int[] rowYOffsets = computeOffsets(rowHeadersData, rowGroups, false);
            rowBreakable = computeBreakableHeaders(rowHeadersData, rowGroups, rowYOffsets, false, false);
            rowCount = computeCounts(rowHeadersData);

            spanHeaders = new HeaderCell[rowGroups.length - 1];
            spanHeadersStart = new int[rowGroups.length - 1];

            startRowIndex = 0;
            startColumnIndex = 0;
            lastColumnIndex = 0;
            noDataCellPrinted = false;
            titlePrinted = false;
        }

        protected void setRowHeadersXOffsets() {
            rowHeadersXOffsets = new int[rowGroups.length + 1];
            rowHeadersXOffsets[0] = 0;
            for (int i = 0; i < rowGroups.length; i++) {
                rowHeadersXOffsets[i + 1] = rowHeadersXOffsets[i] + rowGroups[i].getWidth();
            }
        }

        protected int[] computeOffsets(HeaderCell[][] headersData, JRFillCrosstabGroup[] groups, boolean width) {
            int[] offsets = new int[headersData[0].length + 1];
            offsets[0] = 0;
            for (int i = 0; i < headersData[0].length; i++) {
                int size = 0;
                for (int j = groups.length - 1; j >= 0; --j) {
                    if (headersData[j][i] != null) {
                        JRFillCellContents cell = headersData[j][i].isTotal() ? groups[j].getFillTotalHeader()
                                : groups[j].getFillHeader();
                        size = cell == null ? 0 : (width ? cell.getWidth() : cell.getHeight());
                        break;
                    }
                }

                offsets[i + 1] = offsets[i] + size;
            }

            return offsets;
        }

        protected boolean[] computeBreakableHeaders(HeaderCell[][] headersData, JRFillCrosstabGroup[] groups,
                int[] offsets, boolean width, boolean startHeaders) {
            boolean[] breakable = new boolean[headersData[0].length];
            for (int i = 0; i < breakable.length; i++) {
                breakable[i] = true;
            }

            for (int j = 0; j < groups.length; ++j) {
                JRFillCellContents fillHeader = groups[j].getFillHeader();

                if (fillHeader != null) {
                    int size = width ? fillHeader.getWidth() : fillHeader.getHeight();

                    for (int i = 0; i < headersData[0].length; i++) {
                        HeaderCell header = headersData[j][i];
                        if (header != null && !header.isTotal() && header.getLevelSpan() > 1) {
                            int span = header.getLevelSpan();

                            if (startHeaders) {
                                for (int k = i + 1; k < i + span && offsets[k] - offsets[i] < size; ++k) {
                                    breakable[k] = false;
                                }
                            }

                            for (int k = i + span - 1; k > i && offsets[i + span] - offsets[k] < size; --k) {
                                breakable[k] = false;
                            }
                        }
                    }
                }
            }

            return breakable;
        }

        private int[] computeCounts(HeaderCell[][] headersData) {
            int[] counts = new int[headersData[0].length];

            HeaderCell[] lastHeaders = headersData[headersData.length - 1];
            for (int i = 0, c = 0; i < counts.length; ++i) {
                HeaderCell lastHeader = lastHeaders[i];
                if (lastHeader != null && !lastHeader.isTotal()) {
                    ++c;
                }

                counts[i] = c;
            }

            return counts;
        }

        protected void fill(int availableHeight) throws JRException {
            printRows.clear();

            yOffset = 0;
            yChunkOffset = 0;
            willOverflow = false;

            fillVerticalCrosstab(availableHeight);
        }

        protected boolean willOverflow() {
            return willOverflow;
        }

        protected boolean ended() {
            return hasData
                    ? (startRowIndex >= rowHeadersData[0].length && startColumnIndex >= columnHeadersData[0].length)
                    : noDataCellPrinted;
        }

        protected void fillVerticalCrosstab(int availableHeight) throws JRException {
            JRLineBox lineBox = getLineBox();
            int padding = lineBox.getTopPadding() + lineBox.getBottomPadding();
            int contentsHeight = availableHeight - padding;

            if (contentsHeight < 0) {
                willOverflow = true;
                return;
            }

            if (!hasData) {
                fillNoDataCell(contentsHeight);
                if (!printRows.isEmpty()) {
                    addFilledRows();
                }
                return;
            }

            printRowHeaders = startColumnIndex == 0 || isRepeatRowHeaders();
            int rowHeadersXOffset = printRowHeaders ? rowHeadersXOffsets[rowGroups.length] : 0;

            if (startColumnIndex == lastColumnIndex) {
                int availableWidth;
                if (ignoreWidth) {
                    //FIXME lucianc this assumes that the crosstab is not placed in a subreport or table
                    availableWidth = filler.maxPageWidth - filler.leftMargin - filler.rightMargin - getX()
                            - lineBox.getLeftPadding() - lineBox.getRightPadding();
                } else {
                    availableWidth = getWidth() - lineBox.getLeftPadding() - lineBox.getRightPadding();
                }

                columnHeaders = getGroupHeaders(availableWidth - rowHeadersXOffset, columnXOffsets, columnBreakable,
                        startColumnIndex, columnHeadersData, columnGroups);
                lastColumnIndex = startColumnIndex + columnHeaders.size();

                if (startColumnIndex == lastColumnIndex) {
                    throw new JRRuntimeException(EXCEPTION_MESSAGE_KEY_NOT_ENOUGH_SPACE, (Object[]) null);
                }
            }

            List<JRPrintElement> titleRow = null;
            if (startRowIndex == 0 && startColumnIndex == 0 && titleCellContents != null && !titlePrinted) {
                titleRow = fillTitleCell(contentsHeight - yOffset);
                if (willOverflow) {
                    return;
                }
                titlePrinted = true;
            }

            if (interactive && measures.length > 1) {
                dataColumnSortMeasures = new int[lastColumnIndex - startColumnIndex];
                Arrays.fill(dataColumnSortMeasures, -1);
            }

            printColumnHeaders = startRowIndex == 0 || isRepeatColumnHeaders();
            List<List<JRPrintElement>> columnHeaderRows = null;
            if (printColumnHeaders) {
                columnHeaderRows = fillColumnHeaders(rowHeadersXOffset, contentsHeight - yOffset);
                if (willOverflow) {
                    //FIXME avoid repeating this
                    if (titleRow != null) {
                        addPrintRow(titleRow);
                        addFilledRows();
                    }

                    return;
                }
            }

            int lastRowIndex = fillRows(rowHeadersXOffset, contentsHeight - yOffset);

            if (lastRowIndex == startRowIndex) {
                willOverflow = true;

                if (titleRow != null) {
                    addPrintRow(titleRow);
                    addFilledRows();
                }

                return;
            }

            if (titleRow != null) {
                addPrintRow(titleRow);
            }

            if (columnHeaderRows != null) {
                printRows.addAll(columnHeaderRows);
            }

            if (!printRows.isEmpty()) {
                addFilledRows();
            }

            if (lastRowIndex >= rowHeadersData[0].length) {
                startColumnIndex = lastColumnIndex;

                if (startColumnIndex < columnHeadersData[0].length) {
                    startRowIndex = lastRowIndex = 0;

                    // set the chunk offset and compute the remaining height
                    int yAdvance = yOffset + getColumnBreakOffset() + padding;
                    yChunkOffset += yAdvance;
                    int remainingHeight = availableHeight - yAdvance;

                    // reset the elements offset
                    yOffset = 0;

                    // fill a new chunk
                    fillVerticalCrosstab(remainingHeight);
                    return;
                }
            }

            //FIXME is this needed?  we do setPrepareHeight in prepare()
            boolean fillEnded = lastRowIndex >= rowHeadersData[0].length
                    && lastColumnIndex >= columnHeadersData[0].length;
            if (fillEnded) {
                setPrepareHeight(yOffset);
            } else {
                setPrepareHeight(availableHeight);
            }

            startRowIndex = lastRowIndex;

            willOverflow = !fillEnded;
        }

        protected void addFilledRows() {
            List<JRPrintElement> prints = new ArrayList<JRPrintElement>();
            for (Iterator<List<JRPrintElement>> it = printRows.iterator(); it.hasNext();) {
                List<JRPrintElement> rowPrints = it.next();
                prints.addAll(rowPrints);
            }

            // add the crosstab chunk to the element
            addCrosstabChunk(prints, yChunkOffset);

            // clear the added rows
            printRows.clear();
        }

        protected List<HeaderCell[]> getGroupHeaders(int available, int[] offsets, boolean[] breakable,
                int firstIndex, HeaderCell[][] headersData, JRFillCrosstabGroup[] groups) {
            List<HeaderCell[]> headers = new ArrayList<HeaderCell[]>();

            int maxOffset = available + offsets[firstIndex];
            int lastIndex;
            for (lastIndex = firstIndex; lastIndex < headersData[0].length
                    && offsets[lastIndex + 1] <= maxOffset; ++lastIndex) {
                HeaderCell[] groupHeaders = new HeaderCell[groups.length];

                for (int j = 0; j < groups.length; ++j) {
                    groupHeaders[j] = headersData[j][lastIndex];
                }

                headers.add(groupHeaders);
            }

            if (lastIndex < headersData[0].length) {
                while (lastIndex > firstIndex && !breakable[lastIndex]) {
                    --lastIndex;
                    headers.remove(headers.size() - 1);
                }
            }

            if (lastIndex > firstIndex) {
                if (firstIndex > 0) {
                    HeaderCell[] firstHeaders = headers.get(0);

                    for (int j = 0; j < groups.length; ++j) {
                        HeaderCell header = headersData[j][firstIndex];

                        if (header == null) {
                            int spanIndex = getSpanIndex(firstIndex, j, headersData);
                            if (spanIndex >= 0) {
                                HeaderCell spanCell = headersData[j][spanIndex];
                                int headerEndIdx = spanCell.getLevelSpan() + spanIndex;
                                if (headerEndIdx > lastIndex) {
                                    headerEndIdx = lastIndex;
                                }
                                firstHeaders[j] = HeaderCell.createLevelSpanCopy(spanCell,
                                        headerEndIdx - firstIndex);
                            }
                        }
                    }
                }

                if (lastIndex < headersData[0].length) {
                    for (int j = 0; j < groups.length; ++j) {
                        HeaderCell header = headersData[j][lastIndex];

                        if (header == null) {
                            int spanIndex = getSpanIndex(lastIndex, j, headersData);
                            if (spanIndex >= firstIndex) {
                                HeaderCell spanCell = headersData[j][spanIndex];
                                HeaderCell[] headerCells = headers.get(spanIndex - firstIndex);
                                headerCells[j] = HeaderCell.createLevelSpanCopy(spanCell, lastIndex - spanIndex);
                            }
                        }
                    }
                }
            }

            return headers;
        }

        protected int getSpanIndex(int i, int j, HeaderCell[][] headersData) {
            int spanIndex = i - 1;
            while (spanIndex >= 0 && headersData[j][spanIndex] == null) {
                --spanIndex;
            }

            if (spanIndex >= 0) {
                HeaderCell spanCell = headersData[j][spanIndex];
                int span = spanCell.getLevelSpan();

                if (span > i - spanIndex) {
                    return spanIndex;
                }
            }

            return -1;
        }

        protected void fillNoDataCell(int availableHeight) throws JRException {
            if (whenNoDataCell == null) {
                noDataCellPrinted = true;
            } else {
                if (availableHeight < whenNoDataCell.getHeight()) {
                    willOverflow = true;
                } else {
                    whenNoDataCell.evaluate(JRExpression.EVALUATION_DEFAULT);
                    whenNoDataCell.prepare(availableHeight);

                    willOverflow = whenNoDataCell.willOverflow();

                    if (!willOverflow) {
                        whenNoDataCell.setX(0);
                        whenNoDataCell.setY(0);

                        JRPrintFrame printCell = whenNoDataCell.fill();
                        List<JRPrintElement> noDataRow = new ArrayList<JRPrintElement>(1);
                        noDataRow.add(printCell);
                        addPrintRow(noDataRow);

                        yOffset += whenNoDataCell.getPrintHeight();
                        noDataCellPrinted = true;
                    }
                }
            }
        }

        protected List<List<JRPrintElement>> fillColumnHeaders(int rowHeadersXOffset, int availableHeight)
                throws JRException {
            JRFillCellContents[][] columnHeaderRows = new JRFillCellContents[columnGroups.length][lastColumnIndex
                    - startColumnIndex + 2];

            rowYs.clear();
            rowYs.add(0);

            if (printRowHeaders && headerCell != null) {
                JRFillCellContents contents = fillHeader(availableHeight);

                if (willOverflow) {
                    return null;
                }

                columnHeaderRows[columnGroups.length - 1][0] = contents;
            }

            rows: for (rowIdx = 0; rowIdx < columnGroups.length; rowIdx++) {
                if (printRowHeaders) {
                    JRFillCellContents cell = prepareColumnCrosstabHeader(availableHeight);
                    columnHeaderRows[rowIdx][1] = cell;
                    if (willOverflow) {
                        break rows;
                    }
                }

                for (int columnIdx = startColumnIndex; columnIdx < lastColumnIndex; ++columnIdx) {
                    HeaderCell[] headers = columnHeaders.get(columnIdx - startColumnIndex);
                    HeaderCell cell = headers[rowIdx];

                    if (cell != null) {
                        JRFillCellContents contents = prepareColumnHeader(cell, rowIdx, columnIdx,
                                rowHeadersXOffset, availableHeight);
                        columnHeaderRows[rowIdx + cell.getDepthSpan() - 1][columnIdx - startColumnIndex
                                + 2] = contents;

                        if (willOverflow) {
                            break rows;
                        }
                    }
                }

                int rowStretchHeight = stretchColumnHeadersRow(columnHeaderRows[rowIdx]);
                rowYs.add(rowYs.get(rowIdx) + rowStretchHeight);
            }

            List<List<JRPrintElement>> headerRows;
            if (willOverflow) {
                headerRows = null;
                releaseColumnHeaderCells(columnHeaderRows);
            } else {
                headerRows = fillColumnHeaders(columnHeaderRows);
                // reversing so that the overall header cell comes before the column crosstab header
                Collections.reverse(headerRows);

                yOffset += rowYs.get(columnGroups.length);
            }

            resetVariables();

            return headerRows;
        }

        private void setCountVars(int rowIdx, int colIdx) {
            if (rowIdx == -1) {
                rowCountVar.setValue(null);
            } else {
                rowCountVar.setValue(rowCount[rowIdx]);
            }

            if (colIdx == -1) {
                colCountVar.setValue(null);
            } else {
                colCountVar.setValue(columnCount[colIdx]);
            }
        }

        private List<JRPrintElement> fillTitleCell(int availableHeight) throws JRException {
            setCountVars(-1, -1);

            JRFillCellContents cell = titleCellContents;
            int width = rowHeadersXOffsets[rowGroups.length] + columnXOffsets[lastColumnIndex]
                    - columnXOffsets[startColumnIndex];
            cell = cell.getTransformedContents(width, cell.getHeight(), getTitleCell().getContentsPosition(),
                    CrosstabRowPositionEnum.TOP);

            JRFillCellContents contents = cell.getWorkingClone();
            contents.evaluate(JRExpression.EVALUATION_DEFAULT);
            contents.prepare(availableHeight);

            willOverflow = contents.willOverflow();
            if (willOverflow) {
                return null;
            }

            contents.setX(0);
            contents.setY(yOffset);
            contents.setHorizontalSpan(rowGroups.length + lastColumnIndex - startColumnIndex);
            contents.setVerticalSpan(1);

            JRPrintFrame printCell = contents.fill();
            List<JRPrintElement> titleRow = new ArrayList<JRPrintElement>(1);
            titleRow.add(printCell);

            yOffset += contents.getPrintHeight();

            return titleRow;
        }

        private JRFillCellContents fillHeader(int availableHeight) throws JRException {
            setCountVars(-1, -1);

            JRFillCellContents cell = headerCell;
            if (!headerCell.getChildren().isEmpty() && interactive) {
                // look for row group column headers
                cell = decorateCellWithRowGroupIconLabel(cell);
            }

            JRFillCellContents contents = cell.getWorkingClone();
            contents.evaluate(JRExpression.EVALUATION_DEFAULT);
            contents.prepare(availableHeight);

            if (interactive) {
                contents.addHtmlClass(HTML_CLASS_CROSS_FLOATING);
            }

            willOverflow = contents.willOverflow();

            if (!willOverflow) {
                contents.setX(0);
                contents.setY(yOffset);
                contents.setVerticalSpan(columnGroups.length);
                contents.setHorizontalSpan(rowGroups.length);
            }
            return contents;
        }

        private JRFillCellContents decorateCellWithRowGroupIconLabel(JRFillCellContents cell) {
            List<JRChild> cellElements = cell.getChildren();
            BucketDefinition[] rowBuckets = bucketingService.getRowBuckets();
            int[] headerTextIndices = new int[rowBuckets.length];
            Arrays.fill(headerTextIndices, -1);
            boolean[] alignedText = new boolean[rowBuckets.length];
            boolean foundHeader = false;
            for (int bucketIdx = 0, bucketXOffset = 0; bucketIdx < rowBuckets.length; bucketXOffset += rowGroups[bucketIdx]
                    .getWidth(), bucketIdx++) {
                int headerIndex = findRowGroupColumHeaderElementIndex(bucketIdx, cellElements);
                if (headerIndex < 0) {
                    continue;
                }

                JRElement headerElement = (JRElement) cellElements.get(headerIndex);
                boolean aligned = headerElement.getX() == bucketXOffset
                        && headerElement.getWidth() == rowGroups[bucketIdx].getWidth();

                BucketDefinition rowBucket = rowBuckets[bucketIdx];
                if (bucketOrder(rowBucket) == BucketOrder.NONE && !aligned) {
                    continue;
                }

                headerTextIndices[bucketIdx] = headerIndex;
                alignedText[bucketIdx] = aligned;
                foundHeader = true;
            }

            if (!foundHeader) {
                return cell;
            }

            JRFillCellContents decoratedCell = (JRFillCellContents) cell.createClone();
            List<JRChild> clonedElements = decoratedCell.getChildren();

            BuiltinExpressionEvaluatorFactory builtinExpressions = new BuiltinExpressionEvaluatorFactory();
            JRFillExpressionEvaluator decoratedEvaluator = builtinExpressions.decorate(cell.expressionEvaluator);

            NavigableMap<Integer, JRDesignComponentElement> iconLabelElements = new TreeMap<Integer, JRDesignComponentElement>();
            for (int bucketIdx = 0; bucketIdx < headerTextIndices.length; bucketIdx++) {
                int textElementIndex = headerTextIndices[bucketIdx];
                if (textElementIndex >= 0) {
                    JRFillTextElement textElement = (JRFillTextElement) clonedElements.get(textElementIndex);
                    BucketOrder bucketOrder = bucketOrder(rowBuckets[bucketIdx]);
                    if (bucketOrder == BucketOrder.NONE) {
                        if (alignedText[bucketIdx]) {
                            // adding properties so that the text is selected as part of the row group column
                            textElement.setExpressionEvaluator(decoratedEvaluator);
                            textElement.addDynamicProperty(CrosstabInteractiveJsonHandler.PROPERTY_COLUMN_INDEX,
                                    builtinExpressions.createConstantExpression(Integer.toString(bucketIdx)));
                            textElement.addDynamicProperty(HtmlExporter.PROPERTY_HTML_CLASS,
                                    builtinExpressions.createConstantExpression(
                                            "jrxtrowheader jrxtinteractive " + HTML_CLASS_ROW_FLOATING));
                        }
                    } else {
                        textElement.setExpressionEvaluator(decoratedEvaluator);
                        textElement.addDynamicProperty(MatcherExporterFilter.PROPERTY_MATCHER_EXPORT_FILTER_KEY,
                                builtinExpressions.createConstantExpression(
                                        TableReport.TABLE_HEADER_LABEL_MATCHER_EXPORT_KEY));

                        SortOrderEnum order = BucketOrder.toSortOrderEnum(bucketOrder);
                        if (log.isDebugEnabled()) {
                            log.debug("wrapping header element " + textElement.getUUID()
                                    + " in iconLabel for row group " + bucketIdx);
                        }

                        JRDesignComponentElement iconLabelElement = createIconLabelElement(order, textElement,
                                builtinExpressions);
                        if (alignedText[bucketIdx]) {
                            iconLabelElement.getPropertiesMap().setProperty(
                                    CrosstabInteractiveJsonHandler.PROPERTY_COLUMN_INDEX,
                                    Integer.toString(bucketIdx));
                            iconLabelElement.getPropertiesMap().setProperty(HtmlExporter.PROPERTY_HTML_CLASS,
                                    "jrxtrowheader jrxtinteractive " + HTML_CLASS_ROW_FLOATING);
                        }

                        iconLabelElements.put(textElementIndex, iconLabelElement);
                    }
                }
            }

            IconLabelFillObjectFactory factory = new IconLabelFillObjectFactory(fillFactory, decoratedEvaluator);
            for (Entry<Integer, JRDesignComponentElement> entry : iconLabelElements.descendingMap().entrySet()) {
                int elementIndex = entry.getKey();
                JRDesignComponentElement iconLabelElement = entry.getValue();
                JRFillComponentElement fillIconLabelElement = new JRFillComponentElement(filler, iconLabelElement,
                        factory);
                decoratedCell.addElement(elementIndex + 1, fillIconLabelElement);
            }

            return decoratedCell;
        }

        private int findRowGroupColumHeaderElementIndex(int rowGroupIndex, List<JRChild> cellElements) {
            String rowGropIndexStr = Integer.toString(rowGroupIndex);
            int colHeaderTextIndex = -1;
            for (ListIterator<JRChild> elemIt = cellElements.listIterator(cellElements.size()); elemIt
                    .hasPrevious();) {
                JRChild child = elemIt.previous();
                if (child instanceof JRTextElement) {
                    JRFillTextElement textElement = (JRFillTextElement) child;
                    JRElement parentElement = textElement.getParent();
                    String prop = parentElement.hasProperties()
                            ? parentElement.getPropertiesMap().getProperty(PROPERTY_ROW_GROUP_COLUMN_HEADER)
                            : null;
                    if (prop != null && prop.equals(rowGropIndexStr)) {
                        // found it
                        colHeaderTextIndex = elemIt.nextIndex();
                        break;
                    }
                }
            }
            return colHeaderTextIndex;
        }

        private JRFillCellContents prepareColumnHeader(HeaderCell cell, int rowIdx, int columnIdx, int xOffset,
                int availableHeight) throws JRException {
            JRFillCrosstabColumnGroup group = columnGroups[rowIdx];
            JRFillCellContents contents = cell.isTotal() ? group.getFillTotalHeader() : group.getFillHeader();

            int width = columnXOffsets[columnIdx + cell.getLevelSpan()] - columnXOffsets[columnIdx];
            int height = contents.getHeight();

            if (width <= 0 || height <= 0) {
                return null;
            }

            // column header is a cell that is on the last level with a column header and has no row span 
            boolean headerLabel = rowIdx + cell.getDepthSpan() == lastColumnGroupWithHeaderIndex + 1
                    && cell.getLevelSpan() == 1;
            // a column header is a cell that comes after the last level and has no row span
            boolean header = rowIdx + cell.getDepthSpan() >= lastColumnGroupWithHeaderIndex + 1
                    && cell.getLevelSpan() == 1;

            JRFillCellContents preparedContents = null;

            int rowY = rowYs.get(rowIdx);

            if (availableHeight >= rowY + height) {
                setCountVars(-1, columnIdx);
                setGroupVariables(columnGroups, cell.getBucketValues());
                setGroupMeasureVariables(cell, false);

                contents = contents.getTransformedContents(width, height, group.getPositionValue(),
                        CrosstabRowPositionEnum.TOP);
                boolean firstOnRow = columnIdx == startColumnIndex && (!printRowHeaders || headerCell == null);
                contents = contents.getBoxContents(firstOnRow && getRunDirectionValue() == RunDirectionEnum.LTR,
                        firstOnRow && getRunDirectionValue() == RunDirectionEnum.RTL, false);

                // check if the column is sorted
                if (interactive && headerLabel && matchesOrderByColumn(cell)) {
                    contents = decorateWithSortIcon(contents, orderByColumnInfo.getOrder());
                }

                contents = contents.getWorkingClone();

                contents.evaluate(JRExpression.EVALUATION_DEFAULT);
                contents.prepare(availableHeight - rowY);

                if (interactive) {
                    contents.addHtmlClass(HTML_CLASS_COLUMN_FLOATING);

                    boolean sortHeader = false;
                    if (headerLabel && measures.length > 1) {
                        if (measures.length <= 1) {
                            sortHeader = true;
                        } else {
                            // looking for the sorting measure index property in the column header
                            int sortMeasureIdx = determineColumnSortMeasure(contents);
                            dataColumnSortMeasures[columnIdx - startColumnIndex] = sortMeasureIdx;
                            sortHeader = sortMeasureIdx >= 0;
                        }
                    }

                    if (header) {
                        contents.setPrintProperty(CrosstabInteractiveJsonHandler.PROPERTY_COLUMN_INDEX,
                                Integer.toString(columnIdx));
                        contents.addHtmlClass("jrxtcolheader");
                        if (sortHeader) {
                            contents.addHtmlClass("jrxtinteractive");
                        }
                    }
                }

                if (contents.willOverflow()) {
                    willOverflow = true;
                } else {
                    contents.setX(columnXOffsets[columnIdx] - columnXOffsets[startColumnIndex] + xOffset);
                    contents.setY(rowY + yOffset);
                    contents.setVerticalSpan(cell.getDepthSpan());
                    contents.setHorizontalSpan(cell.getLevelSpan());

                    preparedContents = contents;
                }
            } else {
                willOverflow = true;
            }

            return preparedContents;
        }

        private int determineColumnSortMeasure(JRFillCellContents contents) {
            int sortMeasureIdx = -1;
            JRElement[] cellElements = contents.getElements();
            for (JRElement element : cellElements) {
                String sortMeasureIdxProp = element.hasProperties()
                        ? element.getPropertiesMap().getProperty(PROPERTY_COLUMN_HEADER_SORT_MEASURE_INDEX)
                        : null;
                if (sortMeasureIdxProp != null) {
                    sortMeasureIdx = JRPropertiesUtil.asInteger(sortMeasureIdxProp);
                    break;
                }
            }
            return sortMeasureIdx;
        }

        private JRFillCellContents decorateWithSortIcon(JRFillCellContents cell, SortOrderEnum order) {
            // check whether the contents contain a single text element
            JRElement[] elements = cell.getElements();
            if (elements.length != 1) {
                if (log.isDebugEnabled()) {
                    log.debug("order by column header has " + elements.length + " elements");
                }

                return cell;
            }

            if (!(elements[0] instanceof JRTextElement)) {
                if (log.isDebugEnabled()) {
                    log.debug("order by column header has element " + elements[0].getClass().getName());
                }

                return cell;
            }

            // TODO lucianc cache
            JRFillTextElement textElement = (JRFillTextElement) elements[0];
            if (log.isDebugEnabled()) {
                log.debug("wrapping column header element " + textElement.getUUID() + " in iconLabel");
            }

            BuiltinExpressionEvaluatorFactory builtinExpressions = new BuiltinExpressionEvaluatorFactory();
            JRDesignComponentElement iconLabelElement = createIconLabelElement(order, textElement,
                    builtinExpressions);

            JRFillExpressionEvaluator decoratedEvaluator = builtinExpressions.decorate(cell.expressionEvaluator);
            IconLabelFillObjectFactory factory = new IconLabelFillObjectFactory(fillFactory, decoratedEvaluator);
            JRFillComponentElement fillIconLabelElement = new JRFillComponentElement(filler, iconLabelElement,
                    factory);

            JRFillCellContents clonedCell = (JRFillCellContents) cell.createClone();
            clonedCell.addElement(1, fillIconLabelElement);

            JRFillElement clonedTextElement = (JRFillElement) clonedCell.getElements()[0];
            clonedTextElement.setExpressionEvaluator(decoratedEvaluator);
            clonedTextElement.addDynamicProperty(MatcherExporterFilter.PROPERTY_MATCHER_EXPORT_FILTER_KEY,
                    builtinExpressions.createConstantExpression(TableReport.TABLE_HEADER_LABEL_MATCHER_EXPORT_KEY));

            return clonedCell;
        }

        protected JRDesignComponentElement createIconLabelElement(SortOrderEnum order,
                JRFillTextElement textElement, BuiltinExpressionEvaluatorFactory builtinExpressions) {
            JRTextElement parentElement = (JRTextElement) textElement.getParent();

            JRDesignComponentElement designIconLabelElement = IconLabelComponentUtil
                    .getInstance(filler.getJasperReportsContext())
                    .createIconLabelComponentElement(parentElement, textElement);
            IconLabelComponent iconLabelComponent = (IconLabelComponent) designIconLabelElement.getComponent();

            JRDesignTextField labelTextField = (JRDesignTextField) iconLabelComponent.getLabelTextField();
            JRDesignTextField iconTextField = (JRDesignTextField) iconLabelComponent.getIconTextField();

            designIconLabelElement.setStyle(textElement.getInitStyle());
            labelTextField.setStyle(textElement.getInitStyle());
            iconTextField.setStyle(textElement.getInitStyle());

            if (textElement instanceof JRTextField) {
                labelTextField.setExpression(((JRTextField) textElement).getExpression());
            } else if (textElement instanceof JRStaticText) {
                String text = ((JRStaticText) textElement).getText();
                labelTextField.setExpression(builtinExpressions.createConstantExpression(text));
            }

            String iconText = order == SortOrderEnum.ASCENDING
                    ? filler.getPropertiesUtil().getProperty(TableReport.PROPERTY_UP_ARROW_CHAR)
                    : (order == SortOrderEnum.DESCENDING
                            ? filler.getPropertiesUtil().getProperty(TableReport.PROPERTY_DOWN_ARROW_CHAR)
                            : "");
            iconTextField.setExpression(builtinExpressions.createConstantExpression(" " + iconText));

            designIconLabelElement.getPropertiesMap().setProperty(
                    MatcherExporterFilter.PROPERTY_MATCHER_EXPORT_FILTER_KEY,
                    TableReport.TABLE_HEADER_ICON_LABEL_MATCHER_EXPORT_KEY);

            return designIconLabelElement;
        }

        protected JRFillCellContents prepareColumnCrosstabHeader(int availableHeight) throws JRException {
            JRFillCellContents header = columnGroups[rowIdx].getFillCrosstabHeader();
            if (header == null) {
                return null;
            }

            int width = header.getWidth();
            int height = header.getHeight();
            if (width <= 0 || height <= 0) {
                return null;
            }

            int rowY = rowYs.get(rowIdx);
            if (availableHeight < rowY + height) {
                willOverflow = true;
                return null;
            }

            if (rowIdx == columnGroups.length - 1 && !header.getChildren().isEmpty() && interactive) {
                // look for row group column headers in the last column group's header
                header = decorateCellWithRowGroupIconLabel(header);
            }

            JRFillCellContents preparedCell = header.getWorkingClone();
            setCountVars(-1, -1);
            preparedCell.evaluate(JRExpression.EVALUATION_DEFAULT);
            preparedCell.prepare(availableHeight - rowY);

            if (interactive) {
                preparedCell.addHtmlClass(HTML_CLASS_CROSS_FLOATING);
            }

            if (preparedCell.willOverflow()) {
                willOverflow = true;
                return null;
            }

            preparedCell.setX(0);
            preparedCell.setY(rowY + yOffset);
            preparedCell.setHorizontalSpan(rowGroups.length);
            preparedCell.setVerticalSpan(1);

            return preparedCell;
        }

        private int stretchColumnHeadersRow(JRFillCellContents[] headers) {
            int rowY = rowYs.get(rowIdx);

            int rowStretchHeight = 0;
            for (int j = 0; j < headers.length; j++) {
                JRFillCellContents contents = headers[j];

                if (contents != null) {
                    int startRowY = rowY;
                    if (contents.getVerticalSpan() > 1) {
                        startRowY = rowYs.get(rowIdx - contents.getVerticalSpan() + 1);
                    }

                    int height = contents.getPrintHeight() - rowY + startRowY;

                    if (height > rowStretchHeight) {
                        rowStretchHeight = height;
                    }
                }
            }

            for (int j = 0; j < headers.length; j++) {
                JRFillCellContents contents = headers[j];

                if (contents != null) {
                    int startRowY = rowY;
                    if (contents.getVerticalSpan() > 1) {
                        startRowY = rowYs.get(rowIdx - contents.getVerticalSpan() + 1);
                    }

                    contents.stretchTo(rowStretchHeight + rowY - startRowY);
                }
            }

            return rowStretchHeight;
        }

        private List<List<JRPrintElement>> fillColumnHeaders(JRFillCellContents[][] columnHeaderRows)
                throws JRException {
            List<List<JRPrintElement>> headerRows = new ArrayList<List<JRPrintElement>>(columnGroups.length);

            for (int i = 0; i < columnHeaderRows.length; ++i) {
                List<JRPrintElement> headerRow = new ArrayList<JRPrintElement>(lastColumnIndex - startColumnIndex);
                headerRows.add(headerRow);

                for (int j = 0; j < columnHeaderRows[i].length; j++) {
                    JRFillCellContents contents = columnHeaderRows[i][j];

                    if (contents != null) {
                        headerRow.add(contents.fill());
                        contents.releaseWorkingClone();
                    }
                }
            }

            return headerRows;
        }

        private void releaseColumnHeaderCells(JRFillCellContents[][] columnHeaderRows) throws JRException {
            for (int i = 0; i < columnHeaderRows.length; ++i) {
                for (int j = 0; j < columnHeaderRows[i].length; j++) {
                    JRFillCellContents contents = columnHeaderRows[i][j];

                    if (contents != null) {
                        contents.rewind();
                        contents.releaseWorkingClone();
                    }
                }
            }
        }

        protected int fillRows(int xOffset, int availableHeight) throws JRException {
            rowYs.clear();
            rowYs.add(0);

            for (rowIdx = 0; rowIdx < cellData.length - startRowIndex; ++rowIdx) {
                initPreparedRow();

                prepareRow(xOffset, availableHeight);

                if (willOverflow) {
                    break;
                }

                fillRow();

                rowYs.add(rowYs.get(rowIdx) + preparedRowHeight);
            }

            if (rowIdx < cellData.length - startRowIndex)//overflow
            {
                releasePreparedRow();

                if (printRowHeaders) {
                    fillContinuingRowHeaders(xOffset, availableHeight);
                }
            }

            yOffset += rowYs.get(rowIdx);

            return rowIdx + startRowIndex;
        }

        private void initPreparedRow() {
            preparedRow.clear();
            preparedRowHeight = 0;
        }

        private void removeFilledRows(int rowsToRemove) {
            if (rowsToRemove > 0) {
                for (int i = 0; i < rowsToRemove; ++i) {
                    printRows.remove(printRows.size() - 1);
                    rowYs.remove(rowYs.size() - 1);
                }

                rowIdx -= rowsToRemove;
            }
        }

        private void releasePreparedRow() throws JRException {
            for (Iterator<JRFillCellContents> it = preparedRow.iterator(); it.hasNext();) {
                JRFillCellContents cell = it.next();
                cell.rewind();
                cell.releaseWorkingClone();
            }

            preparedRow.clear();
        }

        private void fillRow() throws JRException {
            int rowY = rowYs.get(rowIdx);

            List<JRPrintElement> rowPrints = new ArrayList<JRPrintElement>(preparedRow.size());
            for (Iterator<JRFillCellContents> it = preparedRow.iterator(); it.hasNext();) {
                JRFillCellContents cell = it.next();

                int spanHeight = 0;
                if (cell.getVerticalSpan() > 1) {
                    spanHeight = rowY - rowYs.get(rowIdx - cell.getVerticalSpan() + 1);
                }

                cell.stretchTo(preparedRowHeight + spanHeight);
                rowPrints.add(cell.fill());

                cell.releaseWorkingClone();
            }

            addPrintRow(rowPrints);
        }

        private void prepareRow(int xOffset, int availableHeight) throws JRException {
            for (int col = startColumnIndex; col < lastColumnIndex; ++col) {
                CrosstabCell data = cellData[rowIdx + startRowIndex][col];
                boolean overflow = prepareDataCell(data, col, availableHeight, xOffset);

                if (overflow) {
                    willOverflow = true;
                    return;
                }
            }

            resetVariables();

            if (printRowHeaders) {
                for (int j = 0; j < rowGroups.length; j++) {
                    HeaderCell cell = rowHeadersData[j][rowIdx + startRowIndex];
                    int vSpan = 0;
                    if (cell == null) {
                        // if we have a span header
                        if (toCloseRowHeader(j)) {
                            cell = spanHeaders[j];
                            vSpan = cell.getLevelSpan();
                            if (spanHeadersStart[j] < startRowIndex)//continuing from the prev page
                            {
                                vSpan += spanHeadersStart[j] - startRowIndex;
                            }
                        }
                    } else {
                        if (cell.getLevelSpan() > 1) {
                            spanHeaders[j] = cell;
                            spanHeadersStart[j] = rowIdx + startRowIndex;
                            continue;
                        }

                        vSpan = 1;
                    }

                    if (cell != null) {
                        boolean overflow = prepareRowHeader(j, cell, vSpan, availableHeight);
                        if (overflow) {
                            willOverflow = true;
                            return;
                        }
                    }
                }

                // successfully prepared a row, reset the span headers
                for (int j = 0; j < rowGroups.length; j++) {
                    // if we have a span header
                    if (rowHeadersData[j][rowIdx + startRowIndex] == null && toCloseRowHeader(j)) {
                        // reset it
                        spanHeaders[j] = null;
                    }
                }

                resetVariables();
            }
        }

        private boolean prepareDataCell(CrosstabCell data, int column, int availableHeight, int xOffset)
                throws JRException {
            int rowY = rowYs.get(rowIdx);

            JRFillCrosstabCell cell = crossCells[data.getRowTotalGroupIndex()][data.getColumnTotalGroupIndex()];
            JRFillCellContents contents = cell == null ? null : cell.getFillContents();
            if (contents == null || contents.getWidth() <= 0 || contents.getHeight() <= 0) {
                return false;
            }

            boolean overflow = availableHeight < rowY + contents.getHeight();
            if (!overflow) {
                boolean leftEmpty = startColumnIndex != 0 && !isRepeatRowHeaders();
                boolean topEmpty = startRowIndex != 0 && !isRepeatColumnHeaders();

                setCountVars(rowIdx + startRowIndex, column);
                setGroupVariables(rowGroups, data.getRowBucketValues());
                setGroupVariables(columnGroups, data.getColumnBucketValues());
                setMeasureVariables(data);

                boolean firstOnRow = leftEmpty && column == startColumnIndex;
                contents = contents.getBoxContents(firstOnRow && getRunDirectionValue() == RunDirectionEnum.LTR,
                        firstOnRow && getRunDirectionValue() == RunDirectionEnum.RTL, topEmpty && rowIdx == 0);
                contents = contents.getWorkingClone();

                contents.evaluate(JRExpression.EVALUATION_DEFAULT);
                contents.prepare(availableHeight - rowY);

                if (interactive) {
                    contents.setPrintProperty(CrosstabInteractiveJsonHandler.PROPERTY_COLUMN_INDEX,
                            Integer.toString(column));
                    contents.addHtmlClass("jrxtdatacell");
                }

                preparedRow.add(contents);

                overflow = contents.willOverflow();

                if (!overflow) {
                    contents.setX(columnXOffsets[column] - columnXOffsets[startColumnIndex] + xOffset);
                    contents.setY(rowY + yOffset);

                    int rowCellHeight = contents.isLegacyElementStretchEnabled() // this test is probably not needed, but using it just to be safe 
                            ? contents.getPrintHeight()
                            : Math.max(contents.getPrintHeight(), contents.getHeight());
                    if (rowCellHeight > preparedRowHeight) {
                        preparedRowHeight = rowCellHeight;
                    }
                }
            }

            return overflow;
        }

        private boolean prepareRowHeader(int rowGroup, HeaderCell cell, int vSpan, int availableHeight)
                throws JRException {
            JRFillCrosstabRowGroup group = rowGroups[rowGroup];
            JRFillCellContents contents = cell.isTotal() ? group.getFillTotalHeader() : group.getFillHeader();

            if (contents.getWidth() <= 0 || contents.getHeight() <= 0) {
                return false;
            }

            int spanHeight = 0;
            int headerY = rowYs.get(rowIdx - vSpan + 1);
            if (vSpan > 1) {
                spanHeight += rowYs.get(rowIdx) - headerY;
            }
            int rowHeight = spanHeight + preparedRowHeight;

            boolean stretchContents = group.getPositionValue() == CrosstabRowPositionEnum.STRETCH;
            int contentsHeight = stretchContents ? rowHeight : contents.getHeight();

            boolean headerOverflow = availableHeight < headerY + contentsHeight || rowHeight < contents.getHeight();

            if (!headerOverflow) {
                setCountVars(rowIdx + startRowIndex - vSpan + 1, -1);
                setGroupVariables(rowGroups, cell.getBucketValues());
                setGroupMeasureVariables(cell, true);

                if (stretchContents) {
                    contents = contents.getTransformedContents(contents.getWidth(), rowHeight,
                            CrosstabColumnPositionEnum.LEFT, CrosstabRowPositionEnum.STRETCH);
                }
                contents = contents.getBoxContents(false, false,
                        rowIdx + 1 == vSpan && (!printColumnHeaders || headerCell == null));
                contents.getWorkingClone();

                contents.evaluate(JRExpression.EVALUATION_DEFAULT);
                contents.prepare(availableHeight - headerY);

                if (interactive) {
                    contents.addHtmlClass(HTML_CLASS_ROW_FLOATING);

                    contents.setPrintProperty(CrosstabInteractiveJsonHandler.PROPERTY_COLUMN_INDEX,
                            Integer.toString(rowGroup));
                    if (cell.getDepthSpan() == 1) {
                        // marking only unspanned headers for HTML selection 
                        contents.addHtmlClass("jrxtrowheader");
                    }
                }

                preparedRow.add(contents);

                headerOverflow = contents.willOverflow();

                if (!headerOverflow) {
                    contents.setX(rowHeadersXOffsets[rowGroup]);
                    contents.setY(headerY + yOffset);
                    contents.setVerticalSpan(vSpan);
                    contents.setHorizontalSpan(cell.getDepthSpan());

                    int rowCellHeight = contents.getPrintHeight() - spanHeight;
                    if (rowCellHeight > preparedRowHeight) {
                        preparedRowHeight = rowCellHeight;
                    }
                }
            }

            if (headerOverflow) {
                removeFilledRows(vSpan - 1);
            }

            return headerOverflow;
        }

        protected boolean toCloseRowHeader(int rowGroup) {
            return rowGroup < rowGroups.length - 1 && spanHeaders[rowGroup] != null
                    && spanHeaders[rowGroup].getLevelSpan() + spanHeadersStart[rowGroup] == rowIdx + startRowIndex
                            + 1;
        }

        private void removeExceedingSpanHeaders() {
            for (int j = rowGroups.length - 2; j >= 0; --j) {
                if (spanHeaders[j] != null && spanHeadersStart[j] >= rowIdx + startRowIndex) {
                    spanHeaders[j] = null;
                }
            }
        }

        private void setBackSpanHeaders() {
            for (int j = rowGroups.length - 2; j >= 0 && spanHeaders[j] == null; --j) {
                int spanIndex = getSpanIndex(rowIdx + startRowIndex, j, rowHeadersData);

                if (spanIndex >= 0) {
                    spanHeaders[j] = rowHeadersData[j][spanIndex];
                    spanHeadersStart[j] = spanIndex;
                }
            }
        }

        private void fillContinuingRowHeaders(int xOffset, int availableHeight) throws JRException {
            boolean done = false;
            breakCrosstab: do {
                removeExceedingSpanHeaders();

                if (!rowBreakable[rowIdx + startRowIndex]) {
                    removeFilledRows(1);
                    setBackSpanHeaders();
                    continue;
                }

                initPreparedRow();

                //fill continuing headers
                for (int j = 0; j < rowGroups.length - 1; ++j) {
                    if (spanHeaders[j] != null) {
                        boolean headerOverflow = prepareContinuingRowHeader(j, availableHeight);

                        if (headerOverflow) {
                            releasePreparedRow();
                            continue breakCrosstab;
                        }
                    }
                }

                if (!preparedRow.isEmpty()) {
                    int lastRowHeight = rowYs.get(rowIdx) - rowYs.get(rowIdx - 1);

                    if (preparedRowHeight > lastRowHeight)//need to stretch already filled row by refilling
                    {
                        refillLastRow(xOffset, availableHeight);
                    } else {
                        fillContinuingHeaders(lastRowHeight);
                    }
                }

                done = true;
            } while (!done && rowIdx > 0);
        }

        private void fillContinuingHeaders(int lastRowHeight) throws JRException {
            int nextToLastHeaderY = rowYs.get(rowIdx - 1);
            List<JRPrintElement> lastPrintRow = getLastPrintRow();

            for (int j = 0; j < preparedRow.size(); ++j) {
                JRFillCellContents contents = preparedRow.get(j);

                int headerY = rowYs.get(rowIdx - contents.getVerticalSpan());

                contents.stretchTo(nextToLastHeaderY - headerY + lastRowHeight);
                lastPrintRow.add(contents.fill());
                contents.releaseWorkingClone();
            }
        }

        private void refillLastRow(int xOffset, int availableHeight) throws JRException {
            removeFilledRows(1);
            setBackSpanHeaders();

            prepareRow(xOffset, availableHeight);
            fillRow();

            rowYs.add(rowYs.get(rowIdx) + preparedRowHeight);
            ++rowIdx;
        }

        private boolean prepareContinuingRowHeader(int rowGroup, int availableHeight) throws JRException {
            HeaderCell cell = spanHeaders[rowGroup];
            int vSpan = rowIdx + startRowIndex - spanHeadersStart[rowGroup];

            if (spanHeadersStart[rowGroup] < startRowIndex)//continuing from the prev page
            {
                vSpan += spanHeadersStart[rowGroup] - startRowIndex;
            }

            int headerY = rowYs.get(rowIdx - vSpan);
            int lastHeaderY = rowYs.get(rowIdx);
            int headerHeight = lastHeaderY - headerY;
            int nextToLastHeaderY = rowYs.get(rowIdx - 1);
            int stretchHeight = nextToLastHeaderY - headerY;

            JRFillCrosstabRowGroup group = rowGroups[rowGroup];
            JRFillCellContents contents = cell.isTotal() ? group.getFillTotalHeader() : group.getFillHeader();

            boolean stretchContents = group.getPositionValue() == CrosstabRowPositionEnum.STRETCH;
            int contentsHeight = stretchContents ? headerHeight : contents.getHeight();

            boolean headerOverflow = availableHeight < headerY + contentsHeight
                    || headerHeight < contents.getHeight();
            if (!headerOverflow) {
                setCountVars(rowIdx + startRowIndex - vSpan, -1);
                setGroupVariables(rowGroups, cell.getBucketValues());
                setGroupMeasureVariables(cell, true);

                if (stretchContents) {
                    contents = contents.getTransformedContents(contents.getWidth(), headerHeight,
                            CrosstabColumnPositionEnum.LEFT, CrosstabRowPositionEnum.STRETCH);
                }

                contents = contents.getBoxContents(false, false,
                        rowIdx == vSpan && (!printColumnHeaders || headerCell == null));
                contents.getWorkingClone();

                contents.evaluate(JRExpression.EVALUATION_DEFAULT);
                contents.prepare(availableHeight - headerY);

                if (interactive) {
                    contents.addHtmlClass(HTML_CLASS_ROW_FLOATING);

                    contents.setPrintProperty(CrosstabInteractiveJsonHandler.PROPERTY_COLUMN_INDEX,
                            Integer.toString(rowGroup));
                    if (cell.getDepthSpan() == 1) {
                        // marking only unspanned headers for HTML selection 
                        contents.addHtmlClass("jrxtrowheader");
                    }
                }

                preparedRow.add(contents);

                headerOverflow = contents.willOverflow();

                if (!headerOverflow) {
                    contents.setX(rowHeadersXOffsets[rowGroup]);
                    contents.setY(headerY + yOffset);
                    contents.setVerticalSpan(vSpan);//FIXME is this correct?
                    contents.setHorizontalSpan(cell.getDepthSpan());

                    int rowHeight = contents.getPrintHeight() - stretchHeight;
                    if (rowHeight > preparedRowHeight) {
                        preparedRowHeight = rowHeight;
                    }
                }
            }

            if (headerOverflow) {
                removeFilledRows(vSpan);
            }

            return headerOverflow;
        }

        protected void addPrintRow(List<JRPrintElement> printRow) {
            printRows.add(printRow);
        }

        protected List<JRPrintElement> getLastPrintRow() {
            return printRows.get(printRows.size() - 1);
        }

        protected void setGroupVariables(JRFillCrosstabGroup[] groups, Bucket[] bucketValues) {
            for (int i = 0; i < groups.length; i++) {
                Object value = null;
                if (bucketValues[i] != null && !bucketValues[i].isTotal()) {
                    value = bucketValues[i].getValue();
                }
                groups[i].getFillVariable().setValue(value);
            }
        }

        protected void setGroupMeasureVariables(HeaderCell cell, boolean rowGroup) {
            MeasureValue[][] totals = cell.getTotals();
            for (int m = 0; m < measures.length; m++) {
                for (int row = 0; row <= rowGroups.length; row++) {
                    for (int col = 0; col <= columnGroups.length; col++) {
                        // setting the same values for all row/column groups when filling column/row headers
                        MeasureValue[] vals = rowGroup ? totals[row] : totals[col];
                        if (row == rowGroups.length && col == columnGroups.length) {
                            // setting the base measure variable
                            Object value = measureValue(vals, m);
                            measures[m].getFillVariable().setValue(value);
                        } else if (retrieveTotal[row][col]) {
                            JRFillVariable totalVar = totalVariables[row][col][m];
                            Object value = measureValue(vals, m);
                            totalVar.setValue(value);
                        }
                    }
                }
            }
        }

        protected void setMeasureVariables(CrosstabCell cell) {
            MeasureValue[] values = cell.getMesureValues();
            for (int i = 0; i < measures.length; i++) {
                Object value = measureValue(values, i);
                measures[i].getFillVariable().setValue(value);
            }

            MeasureValue[][][] totals = cell.getTotals();
            for (int row = 0; row <= rowGroups.length; row++) {
                for (int col = 0; col <= columnGroups.length; col++) {
                    MeasureValue[] vals = totals[row][col];
                    if (retrieveTotal[row][col]) {
                        for (int m = 0; m < measures.length; m++) {
                            JRFillVariable totalVar = totalVariables[row][col][m];
                            Object value = measureValue(vals, m);
                            totalVar.setValue(value);
                        }
                    }
                }
            }
        }

        protected Object measureValue(MeasureValue[] values, int measureIdx) {
            if (values == null) {
                return null;
            }

            Object value;
            if (measures[measureIdx].getPercentageType() == CrosstabPercentageEnum.GRAND_TOTAL) {
                if (values[measureIdx].isInitialized()) {
                    value = values[measureIdx].getValue();
                } else {
                    value = measures[measureIdx].getPercentageCalculator().calculatePercentage(values[measureIdx],
                            grandTotals[measureIdx]);
                }
            } else {
                value = values[measureIdx].getValue();
            }
            return value;
        }

        protected void resetVariables() {
            for (int i = 0; i < rowGroups.length; i++) {
                rowGroups[i].getFillVariable().setValue(null);
            }

            for (int i = 0; i < columnGroups.length; i++) {
                columnGroups[i].getFillVariable().setValue(null);
            }

            for (int i = 0; i < measures.length; i++) {
                measures[i].getFillVariable().setValue(null);
            }

            for (int row = 0; row <= rowGroups.length; ++row) {
                for (int col = 0; col <= columnGroups.length; ++col) {
                    if (retrieveTotal[row][col]) {
                        for (int i = 0; i < measures.length; i++) {
                            totalVariables[row][col][i].setValue(null);
                        }
                    }
                }
            }
        }
    }

    @Override
    public int getColumnBreakOffset() {
        return parentCrosstab.getColumnBreakOffset();
    }

    @Override
    public boolean isRepeatColumnHeaders() {
        return parentCrosstab.isRepeatColumnHeaders();
    }

    @Override
    public boolean isRepeatRowHeaders() {
        return parentCrosstab.isRepeatRowHeaders();
    }

    @Override
    public JRCrosstabCell[][] getCells() {
        return crossCells;
    }

    @Override
    public JRCellContents getWhenNoDataCell() {
        return whenNoDataCell;
    }

    @Override
    public JRCrosstabParameter[] getParameters() {
        return parameters;
    }

    @Override
    public JRExpression getParametersMapExpression() {
        return parentCrosstab.getParametersMapExpression();
    }

    @Override
    public JRElement getElementByKey(String elementKey) {
        return JRBaseCrosstab.getElementByKey(this, elementKey);
    }

    @Override
    public JRFillCloneable createClone(JRFillCloneFactory factory) {
        //not needed
        return null;
    }

    @Override
    public CrosstabColumnCell getTitleCell() {
        return parentCrosstab.getTitleCell();
    }

    @Override
    public JRCellContents getHeaderCell() {
        return headerCell;
    }

    @Override
    public JRVariable[] getVariables() {
        return variables;
    }

    @Override
    public RunDirectionEnum getRunDirectionValue() {
        return parentCrosstab.getRunDirectionValue();
    }

    @Override
    public void setRunDirection(RunDirectionEnum runDirection) {
        throw new UnsupportedOperationException();
    }

    @Override
    public HorizontalPosition getHorizontalPosition() {
        return parentCrosstab.getHorizontalPosition();
    }

    @Override
    public void setHorizontalPosition(HorizontalPosition horizontalPosition) {
        throw new UnsupportedOperationException();
    }

    @Override
    public JROrigin getOrigin() {
        return getElementOrigin();
    }

    @Override
    public Boolean getIgnoreWidth() {
        return parentCrosstab.getIgnoreWidth();
    }

    @Override
    public void setIgnoreWidth(Boolean ignoreWidth) {
        throw new UnsupportedOperationException();
    }

    /**
     * @deprecated Replaced by {@link #setIgnoreWidth(Boolean)}.
     */
    @Override
    public void setIgnoreWidth(boolean ignoreWidth) {
        throw new UnsupportedOperationException();
    }

    @Override
    public Color getDefaultLineColor() {
        return parentCrosstab.getDefaultLineColor();
    }

    @Override
    public JRLineBox getLineBox() {
        return lineBox;
    }

    @Override
    public JasperReportsContext getJasperReportsContext() {
        return filler.getJasperReportsContext();
    }

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

}