/* * 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) 2006 - 2009 Pentaho Corporation.. All rights reserved. */ package org.pentaho.reporting.designer.core.editor.crosstab; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.pentaho.reporting.engine.classic.core.CrosstabCell; import org.pentaho.reporting.engine.classic.core.CrosstabCellBody; import org.pentaho.reporting.engine.classic.core.CrosstabColumnGroup; import org.pentaho.reporting.engine.classic.core.CrosstabHeader; import org.pentaho.reporting.engine.classic.core.CrosstabOtherGroup; import org.pentaho.reporting.engine.classic.core.CrosstabRowGroup; import org.pentaho.reporting.engine.classic.core.CrosstabSummaryHeader; import org.pentaho.reporting.engine.classic.core.CrosstabTitleHeader; import org.pentaho.reporting.engine.classic.core.Group; import org.pentaho.reporting.engine.classic.core.GroupBody; import org.pentaho.reporting.engine.classic.core.elementfactory.CrosstabBuilder; import org.pentaho.reporting.engine.classic.core.elementfactory.CrosstabDetail; import org.pentaho.reporting.engine.classic.core.elementfactory.CrosstabDimension; import org.pentaho.reporting.engine.classic.core.wizard.ContextAwareDataSchemaModel; import org.pentaho.reporting.libraries.base.util.ArgumentNullException; import org.pentaho.reporting.libraries.base.util.HashNMap; import org.pentaho.reporting.libraries.base.util.ObjectUtilities; import java.util.Iterator; import java.util.LinkedHashMap; /** * A simple support class to preserve some existing information when editing crosstabs. This class preserves details * cells if the details definitions have not changed. This class preserves row and column dimensions if the details of * these dimensions have not changed. */ public class CrosstabEditorBuilder extends CrosstabBuilder { // todo: v2 - try to be more granular on how dimensions are restored. // A change in a label should only regenerate the particular header. private static final Log logger = LogFactory.getLog( CrosstabEditorBuilder.class ); private static class Tuple { private CrosstabDimension dimension; private Group group; private Tuple( final CrosstabDimension dimension, final Group group ) { this.dimension = dimension; this.group = group; } public CrosstabDimension getDimension() { return dimension; } public Group getGroup() { return group; } } private final HashNMap<String, Tuple> predefinedGroups; private final CrosstabCellBody cellBody; private final LinkedHashMap<String, CrosstabEditSupport.DetailsDefinition> details; private Boolean detailsChanged; public CrosstabEditorBuilder( final ContextAwareDataSchemaModel dataSchemaModel, final CrosstabCellBody cellBody, final LinkedHashMap<String, CrosstabEditSupport.DetailsDefinition> details ) { super( dataSchemaModel ); ArgumentNullException.validate( "cellBody", cellBody ); ArgumentNullException.validate( "details", details ); this.details = details; this.predefinedGroups = new HashNMap<String, Tuple>(); this.cellBody = cellBody; } protected boolean isDetailsChanged() { if ( detailsChanged != null ) { return detailsChanged.booleanValue(); } final Iterator<CrosstabDetail> detailsFromBuilder = getDetails().iterator(); final Iterator<CrosstabEditSupport.DetailsDefinition> detailsFromPast = details.values().iterator(); while ( detailsFromBuilder.hasNext() && detailsFromPast.hasNext() ) { final CrosstabDetail next = detailsFromBuilder.next(); final CrosstabDetail detail = detailsFromPast.next().createDetail(); if ( ObjectUtilities.equal( next, detail ) == false ) { logger.debug( String.format( "Details do not match up: [%s] vs [%s]", next, detail ) ); detailsChanged = true; return true; } } if ( detailsFromBuilder.hasNext() ) { logger.debug( String.format( "Detail count does not fit: More current details than past ones." ) ); detailsChanged = true; return true; } if ( detailsFromPast.hasNext() ) { logger.debug( String.format( "Detail count does not fit: More past details than current ones." ) ); detailsChanged = true; return true; } logger.debug( String.format( "Details have not changed. This is good, we can preserve the cells." ) ); detailsChanged = false; return false; } protected CrosstabOtherGroup createOtherGroup( final GroupBody body, final String column ) { final Tuple tuple = predefinedGroups.getLast( column ); final Group other = tuple.getGroup(); if ( other instanceof CrosstabOtherGroup ) { predefinedGroups.remove( column, tuple ); logger.debug( String.format( "Preserving existing other group " + column ) ); // 1:1 mapping, this should be ok as it is .. final CrosstabOtherGroup g = (CrosstabOtherGroup) other.derive( true ); g.setBody( body ); return g; } else { return super.createOtherGroup( body, column ); } } protected CrosstabRowGroup createRowGroup( final CrosstabCellBody cellBody, final GroupBody innerBody, final CrosstabDimension rowDimension ) { final String column = rowDimension.getField(); final Tuple tuple = predefinedGroups.getLast( column ); if ( tuple == null ) { return super.createRowGroup( cellBody, innerBody, rowDimension ); } final Group other = tuple.group; if ( ObjectUtilities.equal( tuple.dimension, rowDimension ) ) { if ( other instanceof CrosstabRowGroup ) { predefinedGroups.remove( column, tuple ); logger.debug( String.format( "Preserving existing row group " + column ) ); final CrosstabRowGroup og = (CrosstabRowGroup) other.derive( true ); og.setBody( innerBody ); createSummaryCells( cellBody, rowDimension ); return og; } else if ( other instanceof CrosstabColumnGroup ) { predefinedGroups.remove( column, tuple ); logger.debug( String.format( "Mapping column group into row group " + column ) ); final CrosstabColumnGroup oc = (CrosstabColumnGroup) other; final CrosstabRowGroup cg = new CrosstabRowGroup( innerBody ); cg.setHeader( (CrosstabHeader) oc.getHeader().derive( true ) ); cg.setTitleHeader( (CrosstabTitleHeader) oc.getTitleHeader().derive( true ) ); cg.setSummaryHeader( (CrosstabSummaryHeader) oc.getSummaryHeader().derive( true ) ); createSummaryCells( cellBody, rowDimension ); return cg; } } else { logger.debug( String.format( "Dimension definition has changed on row dimension " + column ) ); } return super.createRowGroup( cellBody, innerBody, rowDimension ); } protected CrosstabColumnGroup createColumnGroup( final CrosstabCellBody cellBody, final GroupBody innerBody, final CrosstabDimension colDimension ) { final String column = colDimension.getField(); final Tuple tuple = predefinedGroups.getLast( column ); if ( tuple == null ) { return super.createColumnGroup( cellBody, innerBody, colDimension ); } final Group other = tuple.group; if ( ObjectUtilities.equal( tuple.dimension, colDimension ) ) { if ( other instanceof CrosstabColumnGroup ) { predefinedGroups.remove( column, tuple ); logger.debug( String.format( "Preserving existing column group " + column ) ); final CrosstabColumnGroup og = (CrosstabColumnGroup) other.derive( true ); og.setBody( innerBody ); createSummaryCells( cellBody, colDimension ); return og; } else if ( other instanceof CrosstabRowGroup ) { predefinedGroups.remove( column, tuple ); logger.debug( String.format( "Mapping row group into column group " + column ) ); final CrosstabRowGroup oc = (CrosstabRowGroup) other; final CrosstabColumnGroup cg = new CrosstabColumnGroup( innerBody ); cg.setHeader( (CrosstabHeader) oc.getHeader().derive( true ) ); cg.setTitleHeader( (CrosstabTitleHeader) oc.getTitleHeader().derive( true ) ); cg.setSummaryHeader( (CrosstabSummaryHeader) oc.getSummaryHeader().derive( true ) ); createSummaryCells( cellBody, colDimension ); return cg; } } else { logger.debug( String.format( "Dimension definition has changed on column dimension " + column ) ); } return super.createColumnGroup( cellBody, innerBody, colDimension ); } protected CrosstabCell createDetailsCell( final String name, final String rowDim, final String colDim ) { if ( !isDetailsChanged() ) { logger.debug( String.format( "Found existing details cell " + name ) ); final CrosstabCell element = cellBody.findElement( rowDim, colDim ); if ( element != null ) { return element; } } return super.createDetailsCell( name, rowDim, colDim ); } protected CrosstabCellBody createCellBody() { if ( !isDetailsChanged() ) { return cellBody.derive( true ); } else { return super.createCellBody(); } } public void addOtherDimension( final CrosstabOtherGroup other ) { addOtherDimension( other.getField() ); predefinedGroups.add( other.getField(), new Tuple( new CrosstabDimension( other.getField() ), other ) ); } public void addRowDimension( final CrosstabDimension dimension, final CrosstabRowGroup rowGroup ) { addRowDimension( dimension ); predefinedGroups.add( rowGroup.getField(), new Tuple( dimension.clone(), rowGroup ) ); } public void addColumnDimension( final CrosstabDimension dimension, final CrosstabColumnGroup rowGroup ) { addColumnDimension( dimension ); predefinedGroups.add( rowGroup.getField(), new Tuple( dimension.clone(), rowGroup ) ); detailsChanged = null; } public CrosstabBuilder clearDimensions() { final CrosstabBuilder crosstabBuilder = super.clearDimensions(); detailsChanged = null; return crosstabBuilder; } }