/* * 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 - 2016 Object Refinery Ltd, Pentaho Corporation and Contributors.. All rights reserved. */ package org.pentaho.reporting.engine.classic.core.modules.output.table.base; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.pentaho.reporting.engine.classic.core.InvalidReportStateException; import org.pentaho.reporting.engine.classic.core.layout.model.BlockRenderBox; import org.pentaho.reporting.engine.classic.core.layout.model.LayoutNodeTypes; import org.pentaho.reporting.engine.classic.core.layout.model.LogicalPageBox; import org.pentaho.reporting.engine.classic.core.layout.model.ParagraphRenderBox; import org.pentaho.reporting.engine.classic.core.layout.model.RenderBox; import org.pentaho.reporting.engine.classic.core.layout.output.OutputProcessorFeature; import org.pentaho.reporting.engine.classic.core.layout.output.OutputProcessorMetaData; import org.pentaho.reporting.engine.classic.core.layout.process.IterateSimpleStructureProcessStep; import org.pentaho.reporting.engine.classic.core.layout.process.util.ProcessUtility; import org.pentaho.reporting.engine.classic.core.style.BandStyleKeys; import org.pentaho.reporting.libraries.base.config.Configuration; import org.pentaho.reporting.libraries.base.util.GenericObjectTable; import org.pentaho.reporting.libraries.xmlns.common.ParserUtil; /** * After the pagination was able to deriveForAdvance the table-structure (all column and row-breaks are now known), this * second step flattens the layout-tree into a two-dimensional table structure. * * @author Thomas Morgner */ @SuppressWarnings( "HardCodedStringLiteral" ) public class TableContentProducer extends IterateSimpleStructureProcessStep { private static final Log logger = LogFactory.getLog( TableContentProducer.class ); private SheetLayout sheetLayout; private GenericObjectTable<CellMarker> contentBackend; private long maximumHeight; private long maximumWidth; private TableRectangle lookupRectangle; private long pageOffset; private long pageEndPosition; private String sheetName; private int finishedRows; private int filledRows; private int clearedRows; private long contentOffset; private long effectiveHeaderSize; private boolean unalignedPagebands; private boolean headerProcessed; private boolean ellipseAsBackground; private boolean shapesAsContent; private boolean processWatermark; private boolean verboseCellMarkers; private int verboseCellMarkersThreshold; private boolean debugReportLayout; private boolean reportCellConflicts; private boolean failOnCellConflicts; private int sectionDepth; private CellMarker.SectionType sectionType; private OutputProcessorMetaData metaData; public TableContentProducer( final SheetLayout sheetLayout, final OutputProcessorMetaData metaData ) { if ( metaData == null ) { throw new NullPointerException(); } if ( sheetLayout == null ) { throw new NullPointerException(); } this.metaData = metaData; this.processWatermark = metaData.isFeatureSupported( OutputProcessorFeature.WATERMARK_SECTION ); this.unalignedPagebands = metaData.isFeatureSupported( OutputProcessorFeature.UNALIGNED_PAGEBANDS ); this.shapesAsContent = metaData.isFeatureSupported( AbstractTableOutputProcessor.SHAPES_CONTENT ); this.ellipseAsBackground = metaData.isFeatureSupported( AbstractTableOutputProcessor.TREAT_ELLIPSE_AS_RECTANGLE ); updateSheetLayout( sheetLayout ); // DebugLog.log("Table-Size: " + sheetLayout.getRowCount() + " " + sheetLayout.getColumnCount()); final Configuration config = metaData.getConfiguration(); final boolean designTime = metaData.isFeatureSupported( OutputProcessorFeature.DESIGNTIME ); if ( designTime == false ) { this.debugReportLayout = "true" .equals( config .getConfigProperty( "org.pentaho.reporting.engine.classic.core.modules.output.table.base.DebugReportLayout" ) ); this.verboseCellMarkers = "true" .equals( config .getConfigProperty( "org.pentaho.reporting.engine.classic.core.modules.output.table.base.VerboseCellMarkers" ) ); this.verboseCellMarkersThreshold = ParserUtil .parseInt( config .getConfigProperty( "org.pentaho.reporting.engine.classic.core.modules.output.table.base.VerboseCellMarkerThreshold" ), 5000 ); this.reportCellConflicts = "true" .equals( config .getConfigProperty( "org.pentaho.reporting.engine.classic.core.modules.output.table.base.ReportCellConflicts" ) ); this.failOnCellConflicts = "true" .equals( config .getConfigProperty( "org.pentaho.reporting.engine.classic.core.modules.output.table.base.FailOnCellConflicts" ) ); } } public boolean isProcessWatermark() { return processWatermark; } public void setProcessWatermark( final boolean processWatermark ) { this.processWatermark = processWatermark; } protected void updateSheetLayout( final SheetLayout sheetLayout ) { if ( sheetLayout == null ) { throw new NullPointerException(); } this.sheetLayout = sheetLayout; this.maximumHeight = sheetLayout.getMaxHeight(); this.maximumWidth = sheetLayout.getMaxWidth(); if ( this.contentBackend == null ) { this.contentBackend = new GenericObjectTable<CellMarker>( Math.max( 1, sheetLayout.getRowCount() ), Math.max( 1, sheetLayout .getColumnCount() ) ); } this.contentBackend.ensureCapacity( sheetLayout.getRowCount(), sheetLayout.getColumnCount() ); } public String getSheetName() { return sheetName; } public CellMarker.SectionType getSectionType() { return sectionType; } public void compute( final LogicalPageBox logicalPage, final boolean iterativeUpdate ) { // this.iterativeUpdate = iterativeUpdate; // ModelPrinter.print(logicalPage); // this.performOutput = performOutput; this.sheetName = null; if ( unalignedPagebands == false ) { // The page-header and footer area are aligned/shifted within the logical pagebox so that all areas // share a common coordinate system. This also implies, that the whole logical page is aligned content. pageOffset = 0; effectiveHeaderSize = 0; pageEndPosition = logicalPage.getPageEnd(); // Log.debug ("Content Processing " + pageOffset + " -> " + pageEnd); sectionType = CellMarker.SectionType.TYPE_INVALID; if ( startBox( logicalPage ) ) { if ( headerProcessed == false ) { sectionType = CellMarker.SectionType.TYPE_HEADER; if ( isProcessWatermark() ) { startProcessing( logicalPage.getWatermarkArea() ); } final BlockRenderBox headerArea = logicalPage.getHeaderArea(); startProcessing( headerArea ); headerProcessed = true; } sectionType = CellMarker.SectionType.TYPE_NORMALFLOW; processBoxChilds( logicalPage ); if ( iterativeUpdate == false ) { sectionType = CellMarker.SectionType.TYPE_REPEAT_FOOTER; final BlockRenderBox repeatFooterBox = logicalPage.getRepeatFooterArea(); startProcessing( repeatFooterBox ); sectionType = CellMarker.SectionType.TYPE_FOOTER; final BlockRenderBox pageFooterBox = logicalPage.getFooterArea(); startProcessing( pageFooterBox ); } } sectionType = CellMarker.SectionType.TYPE_INVALID; finishBox( logicalPage ); // ModelPrinter.print(logicalPage); } else { // The page-header and footer area are not aligned/shifted within the logical pagebox. // All areas have their own coordinate system starting at (0,0). We apply a manual shift here // so that we dont have to modify the nodes (which invalidates the cache, and therefore is ugly) // Log.debug ("Content Processing " + pageOffset + " -> " + pageEnd); effectiveHeaderSize = 0; pageOffset = logicalPage.getPageOffset(); pageEndPosition = ( logicalPage.getPageEnd() ); sectionType = CellMarker.SectionType.TYPE_INVALID; if ( startBox( logicalPage ) ) { if ( headerProcessed == false ) { sectionType = CellMarker.SectionType.TYPE_HEADER; pageOffset = 0; contentOffset = 0; effectiveHeaderSize = 0; if ( isProcessWatermark() ) { final BlockRenderBox watermarkArea = logicalPage.getWatermarkArea(); pageEndPosition = watermarkArea.getHeight(); startProcessing( watermarkArea ); } final BlockRenderBox headerArea = logicalPage.getHeaderArea(); pageEndPosition = headerArea.getHeight(); startProcessing( headerArea ); contentOffset = headerArea.getHeight(); headerProcessed = true; } sectionType = CellMarker.SectionType.TYPE_NORMALFLOW; pageOffset = logicalPage.getPageOffset(); pageEndPosition = logicalPage.getPageEnd(); effectiveHeaderSize = contentOffset; processBoxChilds( logicalPage ); if ( iterativeUpdate == false ) { pageOffset = 0; sectionType = CellMarker.SectionType.TYPE_REPEAT_FOOTER; final BlockRenderBox repeatFooterArea = logicalPage.getRepeatFooterArea(); final long repeatFooterOffset = contentOffset + ( logicalPage.getPageEnd() - logicalPage.getPageOffset() ); final long repeatFooterPageEnd = repeatFooterOffset + repeatFooterArea.getHeight(); effectiveHeaderSize = repeatFooterOffset; pageEndPosition = repeatFooterPageEnd; startProcessing( repeatFooterArea ); final BlockRenderBox footerArea = logicalPage.getFooterArea(); sectionType = CellMarker.SectionType.TYPE_FOOTER; final long footerPageEnd = repeatFooterPageEnd + footerArea.getHeight(); effectiveHeaderSize = repeatFooterPageEnd; pageEndPosition = footerPageEnd; startProcessing( footerArea ); } } sectionType = CellMarker.SectionType.TYPE_INVALID; finishBox( logicalPage ); // ModelPrinter.print(logicalPage); } if ( iterativeUpdate ) { // DebugLog.log("iterative: Computing commited rows: " + sheetLayout.getRowCount() + " vs. " + // contentBackend.getRowCount()); updateFilledRows(); } else { // Log.debug("Non-iterative: Assuming all rows are commited: " + sheetLayout.getRowCount() + " vs. " + // contentBackend.getRowCount()); // updateFilledRows(); filledRows = getRowCount(); } //Set global table offset logicalPage.setProcessedTableOffset( logicalPage.getPageOffset() + sheetLayout.getYPosition( filledRows ) ); if ( iterativeUpdate == false ) { headerProcessed = false; } } protected void computeDesigntimeConflicts( final RenderBox box ) { pageOffset = 0; effectiveHeaderSize = 0; contentOffset = 0; pageEndPosition = box.getHeight(); contentOffset = 0; contentBackend.clear(); startProcessing( box ); filledRows = getRowCount(); } public RenderBox getContent( final int row, final int column ) { if ( verboseCellMarkers == false || row > verboseCellMarkersThreshold ) { if ( row < finishedRows ) { return null; } } final CellMarker marker = contentBackend.getObject( row, column ); if ( marker == null ) { return null; } return marker.getContent(); } public RenderBox getBackground( final int row, final int column ) { if ( verboseCellMarkers == false || row > verboseCellMarkersThreshold ) { if ( row < finishedRows ) { return null; } } final CellMarker marker = contentBackend.getObject( row, column ); if ( marker instanceof BandMarker ) { final BandMarker bandMarker = (BandMarker) marker; return bandMarker.getBandBox(); } return null; } public CellMarker.SectionType getSectionType( final int row, final int column ) { if ( verboseCellMarkers == false || row > verboseCellMarkersThreshold ) { if ( row < finishedRows ) { return CellMarker.SectionType.TYPE_INVALID; } } final CellMarker marker = contentBackend.getObject( row, column ); if ( marker == null ) { return CellMarker.SectionType.TYPE_INVALID; } return marker.getSectionType(); } public long getContentOffset( final int row, final int column ) { if ( verboseCellMarkers == false || row > verboseCellMarkersThreshold ) { if ( row < finishedRows ) { return 0; } } final CellMarker marker = contentBackend.getObject( row, column ); if ( marker == null ) { return 0; } return marker.getContentOffset(); } public int getRowCount() { return Math.max( contentBackend.getRowCount(), sheetLayout.getRowCount() ); } public int getColumnCount() { return Math.max( contentBackend.getColumnCount(), sheetLayout.getColumnCount() ); } protected boolean startBox( final RenderBox box ) { sectionDepth += 1; if ( isProcessed( box ) ) { return true; } if ( box.isVisible() == false ) { return false; } final long height = box.getHeight(); // // DebugLog.log ("Processing Box " + pageOffset + " " + effectiveHeaderSize + " " + box.getY() + " " + height); // DebugLog.log ("Processing Box " + box); if ( height > 0 ) { if ( ( box.getY() + height ) <= pageOffset ) { return false; } if ( box.getY() >= pageEndPosition ) { return false; } } else { // zero height boxes are always a bit tricky .. if ( ( box.getY() + height ) < pageOffset ) { return false; } if ( box.getY() > pageEndPosition ) { return false; } } // Always process everything .. final long y = box.getY() + effectiveHeaderSize - pageOffset; final long y1 = Math.max( effectiveHeaderSize, y ); final long boxX = box.getX(); final long x1 = Math.max( 0, boxX ); final long y2 = Math.min( y + box.getHeight(), maximumHeight ); final long x2 = Math.min( boxX + box.getWidth(), maximumWidth ); lookupRectangle = sheetLayout.getTableBounds( x1, y1, x2 - x1, y2 - y1, lookupRectangle ); final boolean isContentBox; final Boolean contentBoxHint = box.getContentBox(); if ( Boolean.TRUE.equals( contentBoxHint ) ) { // once a box is marked as content, then there is no need to check further .. isContentBox = contentBoxHint.booleanValue(); } else { if ( ( box.getNodeType() & LayoutNodeTypes.TYPE_BOX_CONTENT ) == LayoutNodeTypes.TYPE_BOX_CONTENT || box.getStaticBoxLayoutProperties().isPlaceholderBox() ) { isContentBox = ProcessUtility.isContent( box, ellipseAsBackground, shapesAsContent ) || metaData.isExtraContentElement( box.getStyleSheet(), box.getAttributes() ); if ( isContentBox ) { box.setContentBox( Boolean.TRUE ); } else { box.setContentBox( Boolean.FALSE ); } box.setContentAge( box.getChangeTracker() ); } else if ( box.getFirstChild() == null ) { // empty boxes are never content ... isContentBox = false; } else { if ( contentBoxHint != null && box.getContentAge() == box.getChangeTracker() ) { isContentBox = contentBoxHint.booleanValue(); } else { // once the element has a isContentBox = ProcessUtility.isContent( box, ellipseAsBackground, shapesAsContent ) || metaData.isExtraContentElement( box.getStyleSheet(), box.getAttributes() ); if ( isContentBox ) { box.setContentBox( Boolean.TRUE ); } else { box.setContentBox( Boolean.FALSE ); } box.setContentAge( box.getChangeTracker() ); } } } if ( isContentBox == false ) { collectSheetStyleData( box ); if ( box.isCommited() ) { box.setFinishedTable( true ); } if ( isProcessed( box ) ) { final int rectX2 = lookupRectangle.getX2(); final int rectY2 = lookupRectangle.getY2(); if ( box.isCommited() == false ) { throw new IllegalStateException(); } // Log.debug("Processing box-cell with bounds (" + x1 + ", " + y1 + ")(" + x2 + ", " + y2 + ")"); // if (rectY2 < finishedRows) // { // // this is a repeated encounter, ignore it .. // } contentBackend.ensureCapacity( rectY2, rectX2 ); final BandMarker bandMarker = new BandMarker( box, sectionType, sectionDepth ); for ( int r = Math.max( lookupRectangle.getY1(), finishedRows ); r < rectY2; r++ ) { for ( int c = lookupRectangle.getX1(); c < rectX2; c++ ) { final CellMarker o = contentBackend.getObject( r, c ); if ( isReplaceableBackground( o, bandMarker ) ) { contentBackend.setObject( r, c, bandMarker ); } } } } return true; } if ( box.isCommited() == false ) { // content-box is not finished yet. // if (iterativeUpdate == false) // { // Log.debug("Still Skipping content-cell with bounds (" + x1 + ", " + y1 + ")(" + x2 + ", " + y2 + ")"); // } return false; } // Log.debug("Processing content-cell with bounds (" + x1 + ", " + y1 + ")(" + x2 + ", " + y2 + ")"); collectSheetStyleData( box ); if ( isCellSpaceOccupied( lookupRectangle ) == false ) { final int rectX2 = lookupRectangle.getX2(); final int rectY2 = lookupRectangle.getY2(); contentBackend.ensureCapacity( rectY2, rectX2 ); final ContentMarker contentMarker = new ContentMarker( box, effectiveHeaderSize - pageOffset, sectionType ); for ( int r = lookupRectangle.getY1(); r < rectY2; r++ ) { for ( int c = lookupRectangle.getX1(); c < rectX2; c++ ) { contentBackend.setObject( r, c, contentMarker ); } } // Setting this content-box to finished has to be done in the actual content-generator. } else { handleContentConflict( box ); box.setFinishedTable( true ); } return true; } protected boolean isProcessed( final RenderBox box ) { return box.isFinishedTable(); } protected boolean isReplaceableBackground( final CellMarker oldMarker, final CellMarker newMarker ) { if ( oldMarker == null ) { return true; } if ( oldMarker.getSectionType() == CellMarker.SectionType.TYPE_INVALID ) { return true; } if ( oldMarker.getSectionType() != newMarker.getSectionType() ) { return false; } if ( oldMarker.getSectionDepth() < newMarker.getSectionDepth() ) { return true; } return false; } protected TableRectangle getLookupRectangle() { return lookupRectangle; } protected boolean isFailOnCellConflicts() { return failOnCellConflicts; } protected void setFailOnCellConflicts( final boolean failOnCellConflicts ) { this.failOnCellConflicts = failOnCellConflicts; } protected void handleContentConflict( final RenderBox box ) { if ( reportCellConflicts ) { logger.debug( "LayoutShift: Offending Content: " + box ); logger.debug( "LayoutShift: Offending Content: " + box.isFinishedTable() ); } if ( failOnCellConflicts ) { throw new InvalidReportStateException( "Cannot export content, discovered overlapping cells." ); } } protected void collectSheetStyleData( final RenderBox box ) { final String sheetName = (String) box.getStyleSheet().getStyleProperty( BandStyleKeys.COMPUTED_SHEETNAME ); if ( this.sheetName == null && sheetName != null ) { this.sheetName = sheetName; } } private boolean isCellSpaceOccupied( final TableRectangle rect ) { final int x2 = rect.getX2(); final int y2 = rect.getY2(); for ( int r = rect.getY1(); r < y2; r++ ) { if ( r < finishedRows ) { logger.debug( "Row (" + r + ") already finished" ); return true; } else { for ( int c = rect.getX1(); c < x2; c++ ) { final Object object = contentBackend.getObject( r, c ); if ( object != null && object instanceof BandMarker == false ) { if ( reportCellConflicts ) { logger.debug( "Cell (" + c + ", " + r + ") already filled: Content in cell: " + object ); } return true; } } } } return false; } public int getFinishedRows() { return finishedRows; } public void clearFinishedBoxes() { final int rowCount = getFilledRows(); final int columnCount = getColumnCount(); if ( debugReportLayout ) { logger.debug( "Request: Clearing rows from " + finishedRows + " to " + rowCount ); } boolean atleastOneRowHasContent = false; int lastRowCleared = clearedRows - 1; for ( int row = finishedRows; row < rowCount; row++ ) { boolean lastRowsUndefined = false; boolean rowHasContent = false; for ( int column = 0; column < columnCount; column++ ) { final CellMarker o = contentBackend.getObject( row, column ); if ( o == null ) { if ( debugReportLayout ) { logger.debug( "maybe Cannot clear row: Cell (" + column + ", " + row + ") is undefined." ); } lastRowsUndefined = true; continue; } else if ( lastRowsUndefined ) { if ( debugReportLayout ) { logger.debug( "Cannot clear row: Inner Cell (" + column + ", " + row + ") is undefined." ); } return; } final boolean b = o.isFinished(); if ( b == false ) { if ( debugReportLayout ) { logger.debug( "Cannot clear row: Cell (" + column + ", " + row + ") is not finished: " + o ); } return; } else { if ( rowHasContent == false && o.getContent() != null ) { rowHasContent = true; } } } // we can only clear rows when there is at least some content. Otherwise we will also clear the // markers for the cell-background on the BandMarker. This sadly eats slightly more memory, but // luckily it will only become an issue if your report is a large assortation of bands with not // a single element of real content. if ( rowHasContent ) { atleastOneRowHasContent = true; finishedRows = row + 1; clearedRows = row + 1; for ( int clearRowNr = lastRowCleared + 1; clearRowNr < finishedRows; clearRowNr++ ) { if ( debugReportLayout ) { logger.debug( "#Cleared row: " + clearRowNr + '.' ); } doClear( columnCount, clearRowNr ); } lastRowCleared = row; } } if ( debugReportLayout ) { logger.debug( "Need to clear row: " + ( lastRowCleared + 1 ) + " - " + filledRows ); } finishedRows = filledRows; if ( atleastOneRowHasContent ) { for ( int clearRowNr = lastRowCleared + 1; clearRowNr < finishedRows; clearRowNr++ ) { if ( debugReportLayout ) { logger.debug( "*Cleared row: " + clearRowNr + '.' ); } doClear( columnCount, clearRowNr ); clearedRows = clearRowNr; } } } private void doClear( final int columnCount, final int clearRowNr ) { if ( verboseCellMarkers && filledRows < verboseCellMarkersThreshold ) { for ( int column = 0; column < columnCount; column++ ) { final Object o = contentBackend.getObject( clearRowNr, column ); final FinishedMarker finishedMarker = new FinishedMarker( String.valueOf( o ) ); contentBackend.setObject( clearRowNr, column, finishedMarker ); } } else { contentBackend.clearRow( clearRowNr ); } } protected void finishBox( final RenderBox box ) { sectionDepth -= 1; } protected void processParagraphChilds( final ParagraphRenderBox box ) { // not needed. } public SheetLayout getSheetLayout() { return sheetLayout; } public int getFilledRows() { return filledRows; } private void updateFilledRows() { final int rowCount = contentBackend.getRowCount(); final int columnCount = getColumnCount(); filledRows = finishedRows; for ( int row = finishedRows; row < rowCount; row++ ) { boolean lastRowsUndefined = false; for ( int column = 0; column < columnCount; column++ ) { final CellMarker o = contentBackend.getObject( row, column ); if ( o == null ) { if ( debugReportLayout ) { logger.debug( "Row: Cell (" + column + ", " + row + ") is undefined." ); } lastRowsUndefined = true; continue; } else if ( lastRowsUndefined ) { if ( debugReportLayout ) { logger.debug( "Row: Inner Cell (" + column + ", " + row + ") is undefined." ); } return; } if ( o.isCommited() == false ) { if ( debugReportLayout ) { logger.debug( "Row: Cell (" + column + ", " + row + ") is not commited." ); } return; } } if ( debugReportLayout ) { logger.debug( "Processable Row: " + filledRows + "." ); } filledRows = row + 1; } if ( debugReportLayout ) { logger.debug( "Processable Rows: " + finishedRows + ' ' + filledRows + '.' ); } } public long getContentRowCount() { return contentBackend.getRowCount(); } protected void processBoxChilds( final RenderBox box ) { if ( box.getLayoutNodeType() == LayoutNodeTypes.TYPE_BOX_PARAGRAPH ) { // not needed. Keep this method empty so that the paragraph childs are *not* processed. return; } super.processBoxChilds( box ); } public void reset( final SheetLayout layout ) { updateSheetLayout( layout ); contentBackend.clear(); pageOffset = 0; effectiveHeaderSize = 0; contentOffset = 0; pageEndPosition = 0; contentOffset = 0; filledRows = 0; } }