/* * 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.layout.process; import org.pentaho.reporting.engine.classic.core.layout.model.BlockRenderBox; import org.pentaho.reporting.engine.classic.core.layout.model.CanvasRenderBox; import org.pentaho.reporting.engine.classic.core.layout.model.FinishedRenderNode; import org.pentaho.reporting.engine.classic.core.layout.model.InlineRenderBox; 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.model.RenderNode; import org.pentaho.reporting.engine.classic.core.layout.model.context.StaticBoxLayoutProperties; import org.pentaho.reporting.engine.classic.core.layout.model.table.TableCellRenderBox; import org.pentaho.reporting.engine.classic.core.layout.model.table.TableColumnGroupNode; import org.pentaho.reporting.engine.classic.core.layout.model.table.TableRenderBox; import org.pentaho.reporting.engine.classic.core.layout.model.table.TableRowRenderBox; import org.pentaho.reporting.engine.classic.core.layout.model.table.TableSectionRenderBox; import org.pentaho.reporting.engine.classic.core.util.InstanceID; /** * This step must not remove boxes that have a manual break attached. * * @author Thomas Morgner */ public class CleanPaginatedBoxesStep extends IterateStructuralProcessStep { private static class TableSectionContext { private TableSectionContext context; private int safeRows; private int expectedNextRowNumber; private TableSectionContext( final TableSectionContext context ) { this.context = context; } public TableSectionContext pop() { return context; } public boolean isProcessingUnsafe() { // Are there any rows that could be removed? If not, take a short cut, and skip the body .. return safeRows <= expectedNextRowNumber; } } private long pageOffset; private long shiftOffset; private InstanceID shiftNode; private TableSectionContext tableSectionContext; public CleanPaginatedBoxesStep() { } public long getPageOffset() { return pageOffset; } public void setPageOffset( final long pageOffset ) { this.pageOffset = pageOffset; } protected long compute( final LogicalPageBox pageBox, final long pageOffset ) { this.shiftOffset = 0; this.pageOffset = pageOffset; if ( startBlockBox( pageBox ) ) { // not processing the header and footer area: they are 'out-of-context' bands processBoxChilds( pageBox ); } finishBlockBox( pageBox ); // Log.debug ("ShiftOffset after clean: " + shiftOffset); return shiftOffset; } public long compute( final LogicalPageBox pageBox ) { return compute( pageBox, pageBox.getPageOffset() ); } public InstanceID getShiftNode() { return shiftNode; } protected void processParagraphChilds( final ParagraphRenderBox box ) { // we do not process the paragraph lines. This should have been done // in the startblock thing and they get re-added anyway as long as the // paragraph is active. } public boolean startCanvasBox( final CanvasRenderBox box ) { return false; } protected boolean startRowBox( final RenderBox box ) { return true; } protected boolean startBlockBox( final BlockRenderBox box ) { return startBlockStyleBox( box ); } protected boolean startTableColumnGroupBox( final TableColumnGroupNode box ) { // never remove table-column declarations. They are lightweight anyway .. return false; } protected boolean startTableRowBox( final TableRowRenderBox box ) { tableSectionContext.expectedNextRowNumber = box.getRowIndex() + 1; // we dont remove cells from table-rows. If a table-row is finished, we may be able to remove the whole // row. However, we can remove contents from table-cells if needed. return true; } protected boolean startTableBox( final TableRenderBox box ) { return true; } protected boolean startTableCellBox( final TableCellRenderBox box ) { return startBlockBox( box ); } protected boolean startTableSectionBox( final TableSectionRenderBox box ) { tableSectionContext = new TableSectionContext( tableSectionContext ); if ( box.getDisplayRole() == TableSectionRenderBox.Role.BODY ) { CleanTableRowsPreparationStep preparationStep = new CleanTableRowsPreparationStep(); tableSectionContext.safeRows = preparationStep.process( box, pageOffset ); tableSectionContext.expectedNextRowNumber = preparationStep.getFirstRowEncountered(); if ( tableSectionContext.isProcessingUnsafe() ) { return false; } return startTableSectionStyleBox( box ); } return false; } protected void finishTableSectionBox( final TableSectionRenderBox box ) { tableSectionContext = tableSectionContext.pop(); } private boolean startBlockStyleBox( final RenderBox box ) { final int nodeType = box.getLayoutNodeType(); if ( nodeType == LayoutNodeTypes.TYPE_BOX_PARAGRAPH ) { return false; } boolean boxOutsideVisibleRange = ( box.getY() + box.getOverflowAreaHeight() ) <= pageOffset; final boolean safeForRemove = ( box.getParentWidowContexts() == 0 ) && boxOutsideVisibleRange; if ( safeForRemove || box.getRestrictFinishedClearOut() == RenderBox.RestrictFinishClearOut.UNRESTRICTED ) { // Next, search the last node that is fully invisible. We collapse all // invisible node into one big box for efficiency reasons. They wont be // visible anyway and thus the result will be the same as if they were // still alive .. final RenderNode firstNode = box.getFirstChild(); RenderNode currentNode = firstNode; RenderNode lastToRemove = null; int orphanLeafCount = 0; int widowLeafCount = 0; while ( currentNode != null && currentNode.isOpen() == false && checkFinishedForNode( currentNode ) ) { if ( ( currentNode.getY() + currentNode.getOverflowAreaHeight() ) > pageOffset ) { // we cant handle that. This node will be visible. So the current last // node is the one we can shrink .. break; } orphanLeafCount = currentNode.getOrphanLeafCount(); widowLeafCount = currentNode.getWidowLeafCount(); lastToRemove = currentNode; currentNode = currentNode.getNext(); } if ( lastToRemove != null ) { removeFinishedNodes( box, firstNode, lastToRemove, orphanLeafCount, widowLeafCount ); } } else { // any kind of restricted element: We can only remove one element at a time and only if the // element is a orphan-leaf element. A orphan-leaf has no children that take part in the // widow/orphan constraint calculation and removing the leaf does not alter the result of the // calculation of the OrphanStep. RenderNode currentNode = box.getFirstChild(); while ( currentNode != null && currentNode.isOpen() == false && checkFinishedForNode( currentNode ) ) { if ( ( currentNode.getY() + currentNode.getOverflowAreaHeight() ) > pageOffset ) { // we cant handle that. This node will be visible. So the current last // node is the one we can shrink .. break; } final RenderNode nodeForRemoval = currentNode; currentNode = currentNode.getNext(); if ( isSafeForRemoval( nodeForRemoval ) ) { removeFinishedNodes( box, nodeForRemoval, nodeForRemoval, nodeForRemoval.getOrphanLeafCount(), nodeForRemoval .getWidowLeafCount() ); } } } return true; } private boolean startTableSectionStyleBox( final RenderBox box ) { final int nodeType = box.getLayoutNodeType(); if ( nodeType == LayoutNodeTypes.TYPE_BOX_PARAGRAPH ) { return false; } boolean boxOutsideVisibleRange = ( box.getY() + box.getOverflowAreaHeight() ) <= pageOffset; final boolean safeForRemove = ( box.getParentWidowContexts() == 0 ) && boxOutsideVisibleRange; if ( safeForRemove || box.getRestrictFinishedClearOut() == RenderBox.RestrictFinishClearOut.UNRESTRICTED ) { // Next, search the last node that is fully invisible. We collapse all // invisible node into one big box for efficiency reasons. They wont be // visible anyway and thus the result will be the same as if they were // still alive .. final RenderNode firstNode = box.getFirstChild(); RenderNode currentNode = firstNode; RenderNode lastToRemove = null; int orphanLeafCount = 0; int widowLeafCount = 0; while ( currentNode != null && currentNode.isOpen() == false && checkFinishedForNode( currentNode ) ) { if ( ( currentNode.getY() + currentNode.getOverflowAreaHeight() ) > pageOffset ) { // we cant handle that. This node will be visible. So the current last // node is the one we can shrink .. break; } if ( currentNode.getRowIndex() >= tableSectionContext.safeRows ) { break; } orphanLeafCount = currentNode.getOrphanLeafCount(); widowLeafCount = currentNode.getWidowLeafCount(); lastToRemove = currentNode; currentNode = currentNode.getNext(); } if ( lastToRemove != null ) { removeFinishedNodes( box, firstNode, lastToRemove, orphanLeafCount, widowLeafCount ); } } else { // any kind of restricted element: We can only remove one element at a time and only if the // element is a orphan-leaf element. A orphan-leaf has no children that take part in the // widow/orphan constraint calculation and removing the leaf does not alter the result of the // calculation of the OrphanStep. RenderNode currentNode = box.getFirstChild(); while ( currentNode != null && currentNode.isOpen() == false && checkFinishedForNode( currentNode ) ) { if ( ( currentNode.getY() + currentNode.getOverflowAreaHeight() ) > pageOffset ) { // we cant handle that. This node will be visible. So the current last // node is the one we can shrink .. break; } if ( currentNode.getRowIndex() >= tableSectionContext.safeRows ) { break; } final RenderNode nodeForRemoval = currentNode; currentNode = currentNode.getNext(); if ( isSafeForRemoval( nodeForRemoval ) ) { removeFinishedNodes( box, nodeForRemoval, nodeForRemoval, nodeForRemoval.getOrphanLeafCount(), nodeForRemoval .getWidowLeafCount() ); } } } return true; } protected boolean checkFinishedForNode( final RenderNode currentNode ) { return currentNode.isFinishedPaginate(); } private boolean isSafeForRemoval( final RenderNode node ) { if ( node.isOrphanLeaf() ) { return true; } if ( node.getRestrictFinishedClearOut() == RenderBox.RestrictFinishClearOut.UNRESTRICTED ) { return true; } if ( ( node.getNodeType() & LayoutNodeTypes.MASK_BOX ) == LayoutNodeTypes.MASK_BOX ) { final RenderBox box = (RenderBox) node; final RenderNode child = box.getFirstChild(); if ( child != null && child == box.getLastChild() ) { if ( child.isOrphanLeaf() ) { return true; } if ( child.getRestrictFinishedClearOut() == RenderBox.RestrictFinishClearOut.UNRESTRICTED ) { return true; } } } return false; } private void removeFinishedNodes( final RenderBox box, final RenderNode firstNode, final RenderNode last, final int orphanLeafCount, final int widowLeafCount ) { if ( last.isOpen() ) { throw new IllegalStateException( "The last node is still open. We should not have come that far." ); } if ( last == firstNode ) { if ( last.getNodeType() == LayoutNodeTypes.TYPE_NODE_FINISHEDNODE ) { // In this case, we can skip the replace-action below .. return; } } // So lets get started. We remove all nodes between (and inclusive) // node and last. final long width = box.getContentAreaX2() - box.getContentAreaX1(); final long lastY2; if ( last.getNext() == null ) { lastY2 = last.getY() + last.getHeight(); } else { // in case the next box had been shifted lastY2 = last.getNext().getY(); } final long startOfBox; final RenderNode prev = firstNode.getPrev(); if ( prev == null ) { final StaticBoxLayoutProperties sblp = box.getStaticBoxLayoutProperties(); final long insetsTop = sblp.getBorderTop() + box.getBoxDefinition().getPaddingTop(); startOfBox = box.getY() + insetsTop; } else { startOfBox = prev.getY() + prev.getHeight(); } final long height = lastY2 - startOfBox; if ( startOfBox + height > pageOffset ) { throw new IllegalStateException( "This finished node will intrude into the visible area." ); } // make sure that the finished-box inherits the margins .. final long marginsTop = firstNode.getEffectiveMarginTop(); final long marginsBottom = last.getEffectiveMarginBottom(); final boolean breakAfter = isBreakAfter( last ); RenderNode removeNode = firstNode; while ( removeNode != last ) { final RenderNode next = removeNode.getNext(); if ( removeNode.isOpen() ) { throw new IllegalStateException( "A node is still open. We should not have come that far." ); } box.remove( removeNode ); removeNode = next; } final FinishedRenderNode replacement = new FinishedRenderNode( box.getContentAreaX1(), startOfBox, width, height, marginsTop, marginsBottom, breakAfter, orphanLeafCount, widowLeafCount ); box.replaceChild( last, replacement ); if ( replacement.getParent() != box ) { // return true; throw new IllegalStateException( "The replacement did not work." ); } final long cachedY2; if ( last.getNext() == null ) { cachedY2 = last.getCachedY() + last.getCachedHeight(); } else { cachedY2 = last.getNext().getCachedY(); } final long newShift = lastY2 - cachedY2; if ( newShift > shiftOffset ) { shiftOffset = newShift; shiftNode = box.getInstanceId(); } } private boolean isBreakAfter( final RenderNode node ) { if ( node.isBreakAfter() ) { return true; } if ( ( node.getLayoutNodeType() & LayoutNodeTypes.MASK_BOX_BLOCK ) == LayoutNodeTypes.MASK_BOX_BLOCK ) { final RenderBox box = (RenderBox) node; final RenderNode lastChild = box.getLastChild(); if ( lastChild != null ) { return isBreakAfter( lastChild ); } } return false; } protected boolean startInlineBox( final InlineRenderBox box ) { return false; } protected boolean startAutoBox( final RenderBox box ) { if ( box.getLayoutNodeType() == LayoutNodeTypes.TYPE_BOX_TABLE_SECTION ) { if ( box.isContainsReservedContent() ) { // never clear out anything from reserved header or footer boxes. Never! return false; } if ( tableSectionContext.isProcessingUnsafe() ) { return false; } return startTableSectionStyleBox( box ); } if ( box.getLayoutNodeType() == LayoutNodeTypes.TYPE_BOX_TABLE ) { return true; } if ( ( box.getLayoutNodeType() & LayoutNodeTypes.MASK_BOX_BLOCK ) == LayoutNodeTypes.MASK_BOX_BLOCK ) { return startBlockStyleBox( box ); } return true; } }