/* * This program is free software; you can redistribute it and/or modify it under the * terms of the GNU Lesser General Public License, version 2.1 as published by the Free Software * Foundation. * * You should have received a copy of the GNU Lesser General Public License along with this * program; if not, you can obtain a copy at http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html * or from the Free Software Foundation, Inc., * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. * * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. * See the GNU Lesser General Public License for more details. * * Copyright (c) 2001 - 2013 Object Refinery Ltd, Pentaho Corporation and Contributors.. All rights reserved. */ package org.pentaho.reporting.engine.classic.core.states.crosstab; import org.pentaho.reporting.engine.classic.core.DataRow; import org.pentaho.reporting.engine.classic.core.InvalidReportStateException; import org.pentaho.reporting.engine.classic.core.states.ReportStateKey; import org.pentaho.reporting.libraries.base.util.ObjectUtilities; import java.util.ArrayList; import java.util.Arrays; import java.util.HashMap; /** * Computed structural data of a crosstab. It basically contains the full dataset of the column axis, which then allows * us to inject artificial rows into the dataset. * <p/> * This mode uses the order in which elements occur in the datastream as normalized order of dimension elements, and if * there is ambiguity, it sorts elements by their natural order as well. * <p/> * We have the assumption, that the data is already pre-sorted in some way and that all rows are given in that order. We * assume that all items are comparable and that the items are sorted according to the natural order of the key. This * mode is intended to be used for raw-data crosstabs only. It consumes less memory than the ordered merge. * * @author Thomas Morgner */ public class SortedMergeCrosstabSpecification implements CrosstabSpecification { private ArrayList<Object[]> entries; private ArrayList<DimensionNode> currentRow; private DimensionNode rootNode; private HashMap<DimensionNode, DimensionNode> existingNodes; private String[] columnSet; private String[] rowSet; private ReportStateKey key; private int rowCount; public SortedMergeCrosstabSpecification( final ReportStateKey key, final String[] dimensionColumnSet, final String[] rowColumnSet ) { if ( key == null ) { throw new NullPointerException(); } if ( dimensionColumnSet == null ) { throw new NullPointerException(); } this.key = key; // todo: Make sure we allow an empty column set. This may produce funny crosstabs, but it must not fail hard. this.columnSet = dimensionColumnSet.clone(); this.rowSet = rowColumnSet.clone(); this.entries = new ArrayList<Object[]>(); this.currentRow = new ArrayList<DimensionNode>(); this.existingNodes = new HashMap<DimensionNode, DimensionNode>(); this.rootNode = new DimensionNode( new Object[0], -1 ); this.rowCount = -1; } public int indexOf( final int start, final Object[] key ) { if ( key == null ) { throw new NullPointerException(); } if ( start < 0 ) { throw new IndexOutOfBoundsException(); } final int size = entries.size(); for ( int i = start; i < size; i++ ) { final Object[] objects = entries.get( i ); if ( ObjectUtilities.equalArray( key, objects ) ) { return i; } } return -1; } public String[] getColumnDimensionNames() { return columnSet.clone(); } public String[] getRowDimensionNames() { return rowSet.clone(); } public ReportStateKey getKey() { return key; } public void startRow() { currentRow.clear(); rowCount += 1; } public void endRow() { boolean modified = false; if ( currentRow.size() > 0 ) { for ( int i = 1; i < currentRow.size(); i++ ) { final DimensionNode dimensionNode = currentRow.get( i ); if ( dimensionNode.addParent( currentRow.get( i - 1 ) ) ) { modified = true; } } if ( currentRow.get( 0 ).addParent( rootNode ) ) { modified = true; } } if ( modified ) { for ( final DimensionNode node : existingNodes.keySet() ) { node.rebalance(); } } } public void endCrosstab() { final DimensionNode[] dimensionNodes = existingNodes.keySet().toArray( new DimensionNode[existingNodes.size()] ); Arrays.sort( dimensionNodes ); this.entries.clear(); for ( int i = 0; i < dimensionNodes.length; i++ ) { final DimensionNode node = dimensionNodes[i]; this.entries.add( node.getData() ); } } public void add( final DataRow dataRow ) { if ( columnSet.length == 0 ) { return; } final Object[] newKey = new Object[columnSet.length]; for ( int i = 0; i < columnSet.length; i++ ) { final String columnName = columnSet[i]; newKey[i] = dataRow.get( columnName ); } if ( currentRow.isEmpty() == false ) { final DimensionNode node = currentRow.get( currentRow.size() - 1 ); if ( Arrays.equals( node.getData(), newKey ) ) { return; } } final DimensionNode node = createUniqueNode( newKey ); if ( currentRow.contains( node ) ) { throw new InvalidReportStateException( "Unsorted column dimension data within a single row-dimension instance." ); } currentRow.add( node ); } private DimensionNode createUniqueNode( final Object[] data ) { final DimensionNode dimensionNode = new DimensionNode( data, rowCount ); final DimensionNode existing = existingNodes.get( dimensionNode ); if ( existing != null ) { return existing; } existingNodes.put( dimensionNode, dimensionNode ); return dimensionNode; } public int size() { return entries.size(); } public Object[] getKeyAt( final int column ) { final Object[] data = entries.get( column ); return data.clone(); } }