/*! * 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) 2002-2016 Pentaho Corporation.. All rights reserved. */ package org.pentaho.reporting.engine.classic.core.layout.output.crosstab; import org.pentaho.reporting.engine.classic.core.Band; 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.Element; import org.pentaho.reporting.engine.classic.core.Group; import org.pentaho.reporting.engine.classic.core.ReportProcessingException; import org.pentaho.reporting.engine.classic.core.event.ReportEvent; import org.pentaho.reporting.engine.classic.core.layout.Renderer; import org.pentaho.reporting.engine.classic.core.layout.build.LayoutModelBuilder; import org.pentaho.reporting.engine.classic.core.layout.model.LayoutNodeTypes; import org.pentaho.reporting.engine.classic.core.layout.model.RenderBox; import org.pentaho.reporting.engine.classic.core.layout.model.RenderBoxNonAutoIterator; import org.pentaho.reporting.engine.classic.core.layout.model.RenderNode; import org.pentaho.reporting.engine.classic.core.layout.model.table.TableCellRenderBox; import org.pentaho.reporting.engine.classic.core.layout.model.table.TableSectionRenderBox; import org.pentaho.reporting.engine.classic.core.layout.output.DefaultOutputFunction; import org.pentaho.reporting.engine.classic.core.layout.style.SimpleStyleSheet; import org.pentaho.reporting.engine.classic.core.style.BandStyleKeys; import org.pentaho.reporting.engine.classic.core.style.ElementStyleKeys; import org.pentaho.reporting.engine.classic.core.style.ResolverStyleSheet; import org.pentaho.reporting.engine.classic.core.style.TableLayout; import org.pentaho.reporting.engine.classic.core.style.resolver.SimpleStyleResolver; import org.pentaho.reporting.engine.classic.core.style.resolver.StyleResolver; import org.pentaho.reporting.engine.classic.core.util.InstanceID; public final class CrosstabOutputHelper { private CrosstabOutputHelper() { } public static TableSectionRenderBox findTableHeaderSection( RenderNode node ) { RenderBox tableBox = null; while ( node != null ) { if ( node.getNodeType() == LayoutNodeTypes.TYPE_BOX_TABLE ) { tableBox = (RenderBox) node; break; } node = node.getParent(); } return getTableSectionRenderBox( tableBox ); } public static TableSectionRenderBox getTableSectionRenderBox( final RenderBox tableBox ) { if ( tableBox == null ) { return null; } final RenderBoxNonAutoIterator it = new RenderBoxNonAutoIterator( tableBox ); while ( it.hasNext() ) { final RenderNode next = it.next(); if ( next.getLayoutNodeType() == LayoutNodeTypes.TYPE_BOX_TABLE_SECTION ) { final TableSectionRenderBox sectionRenderBox = (TableSectionRenderBox) next; if ( sectionRenderBox.getDisplayRole() == TableSectionRenderBox.Role.HEADER ) { return sectionRenderBox; } } } return null; } public static TableSectionRenderBox findTableSection( RenderNode node ) { while ( node != null ) { if ( node.getNodeType() == LayoutNodeTypes.TYPE_BOX_TABLE_SECTION ) { return (TableSectionRenderBox) node; } node = node.getParent(); } return null; } public static RenderNode findNode( final TableSectionRenderBox node, final InstanceID id ) { if ( id == null ) { return null; } if ( node == null ) { return null; } return node.findNodeById( id ); } public static Element createTableCell( final int colSpan, final int rowSpan, final boolean pagebreakBefore, final boolean pagebreakAfter ) { final CrosstabTableCell b = new CrosstabTableCell( colSpan, rowSpan ); b.getStyle().setStyleProperty( BandStyleKeys.PAGEBREAK_BEFORE, pagebreakBefore ); b.getStyle().setStyleProperty( BandStyleKeys.PAGEBREAK_AFTER, pagebreakAfter ); final StyleResolver resolver = new SimpleStyleResolver(); final ResolverStyleSheet resolverTarget = new ResolverStyleSheet(); resolver.resolve( b, resolverTarget ); b.setComputedStyle( new SimpleStyleSheet( resolverTarget ) ); return b; } public static Band createTableRow() { return createTableBand( BandStyleKeys.LAYOUT_TABLE_ROW ); } public static Band createTable( final TableLayout tableLayout ) { final Band b = new Band(); b.getStyle().setStyleProperty( BandStyleKeys.LAYOUT, BandStyleKeys.LAYOUT_TABLE ); b.getStyle().setStyleProperty( ElementStyleKeys.INVISIBLE_CONSUMES_SPACE, true ); b.getStyle().setStyleProperty( BandStyleKeys.TABLE_LAYOUT, tableLayout ); final StyleResolver resolver = new SimpleStyleResolver(); final ResolverStyleSheet resolverTarget = new ResolverStyleSheet(); resolver.resolve( b, resolverTarget ); b.setComputedStyle( new SimpleStyleSheet( resolverTarget ) ); return b; } public static Band createTableBand( final String layout ) { final Band b = new Band(); b.getStyle().setStyleProperty( BandStyleKeys.LAYOUT, layout ); b.getStyle().setStyleProperty( ElementStyleKeys.INVISIBLE_CONSUMES_SPACE, true ); final StyleResolver resolver = new SimpleStyleResolver(); final ResolverStyleSheet resolverTarget = new ResolverStyleSheet(); resolver.resolve( b, resolverTarget ); b.setComputedStyle( new SimpleStyleSheet( resolverTarget ) ); return b; } public static boolean isLastColumnGroup( final ReportEvent event ) { final int gidx = event.getState().getCurrentGroupIndex(); final Group group = event.getReport().getGroup( gidx ); if ( group.getBody() instanceof CrosstabCellBody ) { return true; } return false; } public static boolean closeCrosstabTable( final DefaultOutputFunction outputFunction ) { final RenderedCrosstabLayout crosstabLayout = outputFunction.getCurrentRenderedCrosstabLayout(); if ( crosstabLayout.isCrosstabTableOpen() ) { // close the table. outputFunction.getRenderer().getNormalFlowLayoutModelBuilder().finishBox(); // table-body outputFunction.getRenderer().getNormalFlowLayoutModelBuilder().finishBox(); // table crosstabLayout.setCrosstabTableOpen( false ); return true; } return false; } public static void printCrosstabSummary( final DefaultOutputFunction outputFunction, final ReportEvent event ) throws ReportProcessingException { // column summary is delayed by one level. So when we receive a group-finished for the inner most col-group, // we do not print a summary footer. The summary header for the inner most col-group is printed when the // previous group has finished. // // Example: A crosstab with one column group and one row group. // // Row Col Data // ---------------- // R0 C0 1 // R0 C1 2 // R1 C0 3 // R1 C1 4 // // Both groups have summaries printed. The expected output would be: // // C0 C1 CSum // ------------------- // R0 1 2 3 // R1 3 4 7 // RSum 4 6 10 // // The contents of a single cell can consist of multiple data entries. The (classical) footer // printing does not happen for detail level group. // // The summary along a x-axis is defined in the column groups. The header for CSum is defined in the col-group, // the content for the cell is defined in a cell with the key "Col", and printed when the next group // finishes (here: row group). // // The summary along the y-axis is defined in the row groups. The row group header is defined in the // row-group itself, the content for the summary cell is contained in a cell with the key "Row". The row // summary is printed (in the same way as column groups) with a -1 delay. So in this example, the row sums // would be printed when the crosstab-other groups or crosstab groups finish. // // The total summary (10) does not have a own header, as it is the aggregation of an aggregation. The // contents for the cell are held in a cell with the keys "Row" and "Col" (set; order does not matter). // If that cell does not exist, we search for a row cell, if that does not exist we search for "Col". final int gidx = event.getState().getCurrentGroupIndex() + 1; final Group rawGroup = event.getReport().getGroup( gidx ); if ( rawGroup instanceof CrosstabColumnGroup == false ) { return; } final CrosstabColumnGroup group = (CrosstabColumnGroup) rawGroup; if ( group.isPrintSummary() == false ) { return; } final CrosstabCellBody dataBody = event.getReport().getCrosstabCellBody(); final CrosstabCell element = dataBody.findElement( null, group.getField() ); if ( element == null ) { return; } // handle column summary. This can happen inline, with no new states fired. final RenderedCrosstabLayout crosstabLayout = outputFunction.getCurrentRenderedCrosstabLayout(); final LayoutModelBuilder layoutModelBuilder = outputFunction.getRenderer().getNormalFlowLayoutModelBuilder(); if ( crosstabLayout.isCrosstabHeaderOpen() ) { // Expand all parent group cell-spans by one. expandColumnHeaderSpan( crosstabLayout, layoutModelBuilder, gidx ); // and finally print the title-header and the summary header if ( crosstabLayout.isGenerateColumnTitleHeaders() ) { layoutModelBuilder.startSubFlow( crosstabLayout.getColumnTitleHeaderSubflowId( gidx ) ); createAutomaticCell( layoutModelBuilder ); crosstabLayout.setColumnTitleHeaderCellId( gidx - crosstabLayout.getFirstColGroupIndex(), layoutModelBuilder .dangerousRawAccess().getInstanceId() ); outputFunction.getRenderer().add( group.getTitleHeader(), outputFunction.getRuntime() ); layoutModelBuilder.finishBox(); layoutModelBuilder.suspendSubFlow(); } layoutModelBuilder.startSubFlow( crosstabLayout.getColumnHeaderSubflowId( gidx ) ); createAutomaticCell( layoutModelBuilder ); crosstabLayout.setColumnHeaderCellId( gidx - crosstabLayout.getFirstColGroupIndex(), layoutModelBuilder .dangerousRawAccess().getInstanceId() ); outputFunction.getRenderer().add( group.getSummaryHeader(), outputFunction.getRuntime() ); layoutModelBuilder.finishBox(); layoutModelBuilder.suspendSubFlow(); if ( crosstabLayout.isGenerateMeasureHeaders() ) { layoutModelBuilder.startSubFlow( crosstabLayout.getMeasureHeaderSubflowId() ); createAutomaticCell( layoutModelBuilder ); outputFunction.getRenderer().add( dataBody.getHeader(), outputFunction.getRuntime() ); layoutModelBuilder.finishBox(); layoutModelBuilder.suspendSubFlow(); } } // now print the summary cell. createAutomaticCell( layoutModelBuilder ); layoutModelBuilder.legacyFlagNotEmpty(); outputFunction.getRenderer().startSection( Renderer.SectionType.NORMALFLOW ); outputFunction.getRenderer().add( element, outputFunction.getRuntime() ); outputFunction.addSubReportMarkers( outputFunction.getRenderer().endSection() ); layoutModelBuilder.finishBox(); } public static void expandColumnHeaderSpan( final RenderedCrosstabLayout crosstabLayout, final LayoutModelBuilder layoutModelBuilder, final int gidx ) { final TableSectionRenderBox section = CrosstabOutputHelper.findTableHeaderSection( layoutModelBuilder.dangerousRawAccess() ); for ( int i = crosstabLayout.getFirstColGroupIndex(), count = 0; i < gidx; i += 1, count += 1 ) { if ( crosstabLayout.isGenerateColumnTitleHeaders() ) { final InstanceID columnTitleHeaderId = crosstabLayout.getColumnTitleHeaderCellId( i - crosstabLayout.getFirstColGroupIndex() ); final RenderNode columnTitleHeaderCell = CrosstabOutputHelper.findNode( section, columnTitleHeaderId ); if ( columnTitleHeaderCell instanceof TableCellRenderBox ) { final TableCellRenderBox cellBox = (TableCellRenderBox) columnTitleHeaderCell; cellBox.update( cellBox.getRowSpan(), cellBox.getColSpan() + 1 ); } else { throw new IllegalStateException( "Unable to find node for previous column title header. Aborting report processing." ); } } final InstanceID columnHeaderId = crosstabLayout.getColumnHeaderCellId( i - crosstabLayout.getFirstColGroupIndex() ); final RenderNode columnHeaderCell = CrosstabOutputHelper.findNode( section, columnHeaderId ); if ( columnHeaderCell instanceof TableCellRenderBox ) { final TableCellRenderBox cellBox = (TableCellRenderBox) columnHeaderCell; cellBox.update( cellBox.getRowSpan(), cellBox.getColSpan() + 1 ); } else { throw new IllegalStateException( "Unable to find node for previous column title header. Aborting report processing." ); } } } public static void createAutomaticCell( final LayoutModelBuilder layoutModelBuilder, final int colSpan, final int rowSpan, final Element element ) { final boolean pagebreakBefore = element.getComputedStyle().getBooleanStyleProperty( BandStyleKeys.PAGEBREAK_BEFORE ); final boolean pagebreakAfter = element.getComputedStyle().getBooleanStyleProperty( BandStyleKeys.PAGEBREAK_AFTER ); createAutomaticCell( layoutModelBuilder, colSpan, rowSpan, pagebreakBefore, pagebreakAfter ); } public static void createAutomaticCell( final LayoutModelBuilder layoutModelBuilder ) { createAutomaticCell( layoutModelBuilder, 1, 1, false, false ); } public static void createAutomaticCell( final LayoutModelBuilder layoutModelBuilder, final int colSpan, final int rowSpan ) { createAutomaticCell( layoutModelBuilder, colSpan, rowSpan, false, false ); } private static void createAutomaticCell( final LayoutModelBuilder layoutModelBuilder, final int colSpan, final int rowSpan, final boolean pagebreakBefore, final boolean pagebreakAfter ) { final Element tableCell = createTableCell( colSpan, rowSpan, pagebreakBefore, pagebreakAfter ); layoutModelBuilder.startBox( tableCell ); } public static RenderNode findParentNode( RenderNode renderNode, final InstanceID crosstabId ) { while ( renderNode != null ) { if ( renderNode.getInstanceId() == crosstabId ) { return renderNode; } renderNode = renderNode.getParent(); } return null; } }