/*
* 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.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.pentaho.reporting.engine.classic.core.layout.model.FlowPageBreakPositionList;
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.PageBreakPositionList;
import org.pentaho.reporting.engine.classic.core.layout.model.PageBreakPositions;
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.table.TableSectionRenderBox;
import org.pentaho.reporting.engine.classic.core.layout.process.util.BoxShifter;
import org.pentaho.reporting.engine.classic.core.layout.process.util.FlowPaginationTableState;
import org.pentaho.reporting.engine.classic.core.layout.process.util.InitialPaginationShiftState;
import org.pentaho.reporting.engine.classic.core.layout.process.util.PaginationResult;
import org.pentaho.reporting.engine.classic.core.layout.process.util.PaginationShiftState;
import org.pentaho.reporting.engine.classic.core.layout.process.util.PaginationShiftStatePool;
import org.pentaho.reporting.engine.classic.core.states.ReportStateKey;
/**
* This class uses the concept of shifting to push boxes, which otherwise do not fit on the current page, over the
* page-boundary of the next page.
* <p/>
* We have two shift positions. The normal shift denotes artificial paddings, inserted into the flow where needed to
* move content to the next page. The header-shift is inserted when a repeatable table-header is processed. This header
* reserves a virtual padding area in the infinite-canvas flow to push the next assumed pagebreak to the y2-position of
* the header. A header-shift modifies the pin-position on a box, and modifies where pagebreaks are detected.
*/
public final class FlowPaginationStep extends IterateVisualProcessStep {
private static final Log logger = LogFactory.getLog( FlowPaginationStep.class );
private boolean breakPending;
private FindOldestProcessKeyStep findOldestProcessKeyStep;
private FlowPageBreakPositionList basePageBreakList;
private ReportStateKey visualState;
private FlowPaginationTableState paginationTableState;
private PaginationShiftState shiftState;
private PaginationShiftStatePool shiftStatePool;
private long pageOffsetKey;
private boolean unresolvedWidowReferenceEncountered;
private long recordedPageBreakPosition;
private boolean recordedPageBreakPositionIsForced;
public FlowPaginationStep() {
findOldestProcessKeyStep = new FindOldestProcessKeyStep();
basePageBreakList = new FlowPageBreakPositionList();
shiftStatePool = new PaginationShiftStatePool();
}
public PaginationResult performPagebreak( final LogicalPageBox pageBox ) {
getEventWatch().start();
getSummaryWatch().start();
PaginationStepLib.assertProgress( pageBox );
this.unresolvedWidowReferenceEncountered = false;
this.visualState = null;
this.pageOffsetKey = pageBox.getPageOffset();
this.shiftState = new InitialPaginationShiftState();
this.breakPending = false;
this.recordedPageBreakPosition = 0;
this.recordedPageBreakPositionIsForced = false;
try {
// do not add a pagebreak for the physical end.
final PageBreakPositionList allPreviousBreak = pageBox.getAllVerticalBreaks();
basePageBreakList.copyFrom( allPreviousBreak );
this.paginationTableState = new FlowPaginationTableState( pageBox.getPageOffset(), basePageBreakList );
// now process all the other content (excluding the header and footer area)
if ( startBlockLevelBox( pageBox ) ) {
processBoxChilds( pageBox );
}
finishBlockLevelBox( pageBox );
PaginationStepLib.assertProgress( pageBox );
// reset pagebreaks to state before we performed a pagebreak.
basePageBreakList.copyFrom( allPreviousBreak );
final boolean pagebreakEncountered =
recordedPageBreakPosition != 0
&& ( recordedPageBreakPositionIsForced || recordedPageBreakPosition != pageBox.getHeight() );
final boolean nextPageContainsContent;
if ( pagebreakEncountered == false ) {
nextPageContainsContent = false;
} else {
basePageBreakList.addMajorBreak( recordedPageBreakPosition, 0 );
if ( recordedPageBreakPositionIsForced ) {
nextPageContainsContent = false;
} else {
nextPageContainsContent = ( pageBox.getHeight() > recordedPageBreakPosition );
}
}
return new PaginationResult( basePageBreakList, pagebreakEncountered, nextPageContainsContent, visualState );
} finally {
getEventWatch().stop();
getSummaryWatch().stop( true );
this.paginationTableState = null;
this.visualState = null;
this.shiftState = null;
}
}
protected void processParagraphChilds( final ParagraphRenderBox box ) {
processBoxChilds( box );
}
protected boolean startBlockLevelBox( final RenderBox box ) {
box.setOverflowAreaHeight( box.getCachedHeight() );
final boolean retval = handleStartBlockLevelBox( box );
installTableContext( box );
return retval;
}
private boolean handleStartBlockLevelBox( final RenderBox box ) {
this.shiftState = shiftStatePool.create( box, shiftState );
final long shift = shiftState.getShiftForNextChild();
if ( box.isWidowBox() ) {
unresolvedWidowReferenceEncountered = true;
}
if ( unresolvedWidowReferenceEncountered ) {
// once we have hit a unresolved widow box, we cannot process the page any further
// we have to wait until the box is closed to know whether the widow-constraint can be
// fulfilled.
BoxShifter.shiftBox( box, shift );
return false;
}
PaginationStepLib.assertBlockPosition( box, shift );
if ( shiftState.isManualBreakSuspended() == false ) {
if ( handleManualBreakOnBox( box, shiftState, breakPending ) ) {
breakPending = false;
if ( logger.isDebugEnabled() ) {
logger.debug( "pending page-break or manual break: " + box );
}
return true;
}
breakPending = false;
}
// If this box does not cross any (major or minor) break, it may need no additional shifting at all.
return handleAutomaticPagebreak( box, shiftState );
}
protected void processBlockLevelNode( final RenderNode node ) {
final long shift = shiftState.getShiftForNextChild();
node.setY( node.getY() + shift );
if ( breakPending == false && node.isBreakAfter() ) {
breakPending = paginationTableState.isOnPageStart( node.getY() ) == false;
if ( breakPending ) {
if ( logger.isDebugEnabled() ) {
logger.debug( "BreakPending True for Node:isBreakAfter: " + node );
}
}
}
}
protected void finishBlockLevelBox( final RenderBox box ) {
uninstallTableContext( box );
if ( breakPending == false && box.isBreakAfter() ) {
breakPending = paginationTableState.isOnPageStart( box.getY() + box.getHeight() ) == false;
if ( breakPending ) {
if ( logger.isDebugEnabled() ) {
logger.debug( "BreakPending True for Box:isBreakAfter: " + box );
}
}
}
shiftState = shiftState.pop( box.getInstanceId() );
}
// At a later point, we have to do some real page-breaking here. We should check, whether the box fits, and should
// shift the box if it doesnt.
protected boolean startCanvasLevelBox( final RenderBox box ) {
box.setOverflowAreaHeight( box.getCachedHeight() );
installTableContext( box );
shiftState = shiftStatePool.create( box, shiftState );
shiftState.suspendManualBreaks();
box.setY( box.getY() + shiftState.getShiftForNextChild() );
return true;
}
protected void finishCanvasLevelBox( final RenderBox box ) {
shiftState = shiftState.pop( box.getInstanceId() );
uninstallTableContext( box );
}
protected boolean startRowLevelBox( final RenderBox box ) {
box.setOverflowAreaHeight( box.getCachedHeight() );
installTableContext( box );
shiftState = shiftStatePool.create( box, shiftState );
shiftState.suspendManualBreaks();
box.setY( box.getY() + shiftState.getShiftForNextChild() );
return true;
}
protected void finishRowLevelBox( final RenderBox box ) {
shiftState = shiftState.pop( box.getInstanceId() );
uninstallTableContext( box );
}
protected boolean startTableLevelBox( final RenderBox box ) {
box.setOverflowAreaHeight( box.getCachedHeight() );
if ( box.getNodeType() == LayoutNodeTypes.TYPE_BOX_TABLE_SECTION ) {
final TableSectionRenderBox sectionRenderBox = (TableSectionRenderBox) box;
switch ( sectionRenderBox.getDisplayRole() ) {
case HEADER: {
shiftState = shiftStatePool.create( box, shiftState );
paginationTableState = new FlowPaginationTableState( paginationTableState );
paginationTableState.suspendVisualStateCollection( true );
startTableHeaderSection( box, sectionRenderBox );
return false;
}
case FOOTER: {
shiftState = shiftStatePool.create( box, shiftState );
paginationTableState = new FlowPaginationTableState( paginationTableState );
paginationTableState.suspendVisualStateCollection( true );
// shift the box and all children downwards. Suspend pagebreaks.
final long contextShift = shiftState.getShiftForNextChild();
BoxShifter.shiftBox( box, contextShift );
return false;
}
case BODY:
return startBlockLevelBox( box );
default:
throw new IllegalArgumentException();
}
} else {
return true;
}
}
private void startTableHeaderSection( final RenderBox box, final TableSectionRenderBox sectionRenderBox ) {
final long contextShift = shiftState.getShiftForNextChild();
// shift the header downwards,
// 1. Check that this table actually breaks across the current page. Header position must be
// before the pagebox-offset. If not, return false, after the normal shifting.
final long pageOffset = paginationTableState.getPageOffset();
final long delta = pageOffset - ( sectionRenderBox.getY() + contextShift );
if ( logger.isDebugEnabled() ) {
logger.debug( "PageOffset: " + delta );
}
if ( delta <= 0 ) {
BoxShifter.shiftBox( box, contextShift );
if ( logger.isDebugEnabled() ) {
logger.debug( "HEADER NOT SHIFTED; DELTA = " + delta + " -> " + contextShift );
}
sectionRenderBox.setHeaderShift( pageOffsetKey, 0 );
return;
}
// 2. Shift the whole header downwards so that its upper edge matches the start of the page.
// return false afterwards.
if ( logger.isDebugEnabled() ) {
logger.debug( "HEADER SHIFTED; DELTA = " + delta + " -> " + contextShift );
}
long headerShift = sectionRenderBox.getHeaderShift( pageOffsetKey );
if ( headerShift == 0 ) {
final long previousPageOffset =
paginationTableState.getBreakPositions().findPageStartPositionForPageEndPosition( pageOffset );
headerShift = sectionRenderBox.getHeaderShift( previousPageOffset ) + box.getHeight();
if ( logger.isDebugEnabled() ) {
logger.debug( "HeaderShift: " + headerShift + " <=> " + pageOffset + " ; prevOffset=" + previousPageOffset );
}
sectionRenderBox.setHeaderShift( pageOffsetKey, headerShift );
} else {
if ( logger.isDebugEnabled() ) {
logger.debug( "Existing HeaderShift: " + headerShift + " <=> " + pageOffset );
}
}
if ( logger.isDebugEnabled() ) {
logger.debug( "Table-Height before extension: " + box.getParent().getHeight() );
}
BoxShifter.shiftBox( box, delta );
updateStateKeyDeep( box );
shiftState.increaseShift( headerShift );
if ( logger.isDebugEnabled() ) {
logger.debug( "Table-Height after extension: " + box.getParent().getHeight() );
}
}
protected void finishTableLevelBox( final RenderBox box ) {
if ( box.getNodeType() == LayoutNodeTypes.TYPE_BOX_TABLE_SECTION ) {
final TableSectionRenderBox sectionRenderBox = (TableSectionRenderBox) box;
switch ( sectionRenderBox.getDisplayRole() ) {
case HEADER:
shiftState = shiftState.pop( box.getInstanceId() );
paginationTableState = paginationTableState.pop();
paginationTableState.defineArtificialPageStart( box.getHeight() + paginationTableState.getPageOffset() );
break;
case FOOTER:
shiftState = shiftState.pop( box.getInstanceId() );
paginationTableState = paginationTableState.pop();
break;
case BODY:
finishBlockLevelBox( box );
break;
default:
throw new IllegalStateException();
}
return;
}
finishBlockLevelBox( box );
}
protected boolean startTableSectionLevelBox( final RenderBox box ) {
box.setOverflowAreaHeight( box.getCachedHeight() );
if ( box.getNodeType() == LayoutNodeTypes.TYPE_BOX_TABLE_ROW ) {
if ( box.isOpen() ) {
paginationTableState = new FlowPaginationTableState( paginationTableState );
paginationTableState.suspendVisualStateCollection( false );
}
}
// ignore all other break requests ..
return startBlockLevelBox( box );
}
protected void finishTableSectionLevelBox( final RenderBox box ) {
if ( box.getNodeType() == LayoutNodeTypes.TYPE_BOX_TABLE_ROW ) {
if ( box.isOpen() ) {
paginationTableState = paginationTableState.pop();
}
}
finishBlockLevelBox( box );
}
protected boolean startTableRowLevelBox( final RenderBox box ) {
box.setOverflowAreaHeight( box.getCachedHeight() );
return startRowLevelBox( box );
}
protected void finishTableRowLevelBox( final RenderBox box ) {
finishRowLevelBox( box );
}
protected boolean startTableCellLevelBox( final RenderBox box ) {
box.setOverflowAreaHeight( box.getCachedHeight() );
installTableContext( box );
return startBlockLevelBox( box );
}
protected void finishTableCellLevelBox( final RenderBox box ) {
finishBlockLevelBox( box );
uninstallTableContext( box );
}
protected boolean startInlineLevelBox( final RenderBox box ) {
box.setOverflowAreaHeight( box.getCachedHeight() );
BoxShifter.shiftBox( box, shiftState.getShiftForNextChild() );
return false;
}
protected void processInlineLevelNode( final RenderNode node ) {
node.setY( node.getY() + shiftState.getShiftForNextChild() );
}
protected void finishInlineLevelBox( final RenderBox box ) {
}
protected boolean startTableColLevelBox( final RenderBox box ) {
return false;
}
protected boolean startTableColGroupLevelBox( final RenderBox box ) {
return false;
}
protected void processCanvasLevelNode( final RenderNode node ) {
node.setY( node.getY() + shiftState.getShiftForNextChild() );
}
protected void processRowLevelNode( final RenderNode node ) {
node.setY( node.getY() + shiftState.getShiftForNextChild() );
}
protected void processOtherLevelChild( final RenderNode node ) {
node.setY( node.getY() + shiftState.getShiftForNextChild() );
}
protected void processTableLevelNode( final RenderNode node ) {
node.setY( node.getY() + shiftState.getShiftForNextChild() );
}
protected void processTableRowLevelNode( final RenderNode node ) {
node.setY( node.getY() + shiftState.getShiftForNextChild() );
}
protected void processTableSectionLevelNode( final RenderNode node ) {
node.setY( node.getY() + shiftState.getShiftForNextChild() );
}
protected void processTableCellLevelNode( final RenderNode node ) {
node.setY( node.getY() + shiftState.getShiftForNextChild() );
}
protected void processTableColLevelNode( final RenderNode node ) {
node.setY( node.getY() + shiftState.getShiftForNextChild() );
}
private void updateStateKey( final RenderBox box ) {
if ( paginationTableState.isVisualStateCollectionSuspended() ) {
return;
}
if ( box.getNodeType() == LayoutNodeTypes.TYPE_BOX_TABLE ) {
return;
}
final long y = box.getY();
if ( recordedPageBreakPosition != 0 && y >= recordedPageBreakPosition ) {
return;
}
final ReportStateKey stateKey = box.getStateKey();
if ( stateKey != null && stateKey.isInlineSubReportState() == false ) {
this.visualState = stateKey;
}
}
private void updateStateKeyDeep( final RenderBox box ) {
if ( paginationTableState.isVisualStateCollectionSuspended() ) {
return;
}
final long y = box.getY();
if ( recordedPageBreakPosition != 0 && y >= recordedPageBreakPosition ) {
return;
}
final ReportStateKey reportStateKey = findOldestProcessKeyStep.find( box );
if ( reportStateKey != null && reportStateKey.isInlineSubReportState() == false ) {
this.visualState = reportStateKey;
}
}
private boolean handleAutomaticPagebreak( final RenderBox box, final PaginationShiftState boxContext ) {
final long shift = boxContext.getShiftForNextChild();
final PageBreakPositions breakUtility = paginationTableState.getBreakPositions();
final long boxHeightAndWidowArea =
Math.max( box.getHeight(), PaginationStepLib.getWidowConstraint( box, shiftState, paginationTableState ) );
if ( breakUtility.isCrossingPagebreak( box.getY(), boxHeightAndWidowArea, shift ) == false ) {
// The whole box fits on the current page. No need to do anything fancy.
final RenderBox.BreakIndicator breakIndicator = box.getManualBreakIndicator();
if ( breakIndicator == RenderBox.BreakIndicator.INDIRECT_MANUAL_BREAK ) {
// One of the children of this box will cause a manual pagebreak. We have to dive deeper into this child.
// for now, we will only apply the ordinary shift.
final long boxY = box.getY();
box.setY( boxY + shift );
updateStateKey( box );
return true;
} else { // if (breakIndicator == RenderBox.BreakIndicator.NO_MANUAL_BREAK)
// As neither this box nor any of the children will cause a pagebreak, we can shift them and skip the processing
// from here.
BoxShifter.shiftBox( box, shift );
updateStateKeyDeep( box );
return false;
}
}
// At this point we know, that the box may cause some shifting. It crosses at least one minor or major pagebreak.
// Right now, we are just evaluating the next break. In a future version, we could search all possible break
// positions up to the next major break.
final long boxY = box.getY();
final long boxYShifted = boxY + shift;
final long nextMinorBreak = breakUtility.findNextBreakPosition( boxYShifted );
final long spaceAvailable = nextMinorBreak - boxYShifted;
// This box sits directly on a pagebreak. This means, the page is empty, and there is no need for additional
// shifting.
if ( spaceAvailable == 0 ) {
box.setY( boxYShifted );
updateStateKey( box );
if ( boxYShifted < nextMinorBreak ) {
// this position is shifted, but not header-shifted
box.markPinned( nextMinorBreak );
}
return true;
}
final long spaceConsumed = PaginationStepLib.computeNonBreakableBoxHeight( box, boxContext, paginationTableState );
if ( spaceAvailable < spaceConsumed ) {
// So we have not enough space to fulfill the layout-constraints. Be it so. Lets shift the box to the next
// break.
// check whether we can actually shift the box. We will have to take the previous widow/orphan operations
// into account.
if ( logger.isDebugEnabled() ) {
logger.debug( "Automatic pagebreak, after orphan-opt-out: " + box );
logger.debug( "Automatic pagebreak : " + visualState );
}
final long nextShift = nextMinorBreak - boxY;
box.setY( boxY + nextShift );
boxContext.setShift( nextShift );
updateStateKey( box );
if ( box.getY() < nextMinorBreak ) {
box.markPinned( nextMinorBreak );
}
return true;
}
// OK, there *is* enough space available. Start the normal processing
box.setY( boxYShifted );
updateStateKey( box );
return true;
}
private boolean handleManualBreakOnBox( final RenderBox box, final PaginationShiftState boxContext,
final boolean breakPending ) {
final RenderBox.BreakIndicator breakIndicator = box.getManualBreakIndicator();
// First check the simple cases:
// If the box wants to break, then there's no point in waiting: Shift the box and continue.
if ( breakIndicator != RenderBox.BreakIndicator.DIRECT_MANUAL_BREAK && breakPending == false ) {
return false;
}
final PageBreakPositions breakUtility = paginationTableState.getBreakPositions();
final long shift = boxContext.getShiftForNextChild();
final long boxY = box.getY();
final long shiftedBoxY = boxY + shift;
final long nextMajorBreak = breakUtility.findNextMajorBreakPosition( shiftedBoxY );
if ( nextMajorBreak < shiftedBoxY ) {
// This band will be outside the last pagebreak. We can only shift it normally, but there is no way
// that we could shift it to the final position yet.
box.setY( shiftedBoxY );
} else if ( paginationTableState.isTableProcessing() == false || shiftedBoxY > nextMajorBreak ) {
final long nextShift = nextMajorBreak - boxY;
box.setY( boxY + nextShift );
boxContext.setShift( nextShift );
} else {
box.setY( shiftedBoxY );
}
final long pageEnd = paginationTableState.getPageOffset();
if ( box.getY() <= pageEnd ) {
updateStateKey( box );
box.markPinned( pageEnd );
} else if ( recordedPageBreakPosition == 0 ) {
if ( logger.isDebugEnabled() ) {
logger.debug( "Breaking on box " + box );
}
recordedPageBreakPosition = box.getY();
}
if ( recordedPageBreakPosition == box.getY() && box.getNodeType() == LayoutNodeTypes.TYPE_BOX_BREAKMARK ) {
recordedPageBreakPositionIsForced = true;
}
return true;
}
protected void installTableContext( final RenderBox box ) {
if ( box.getNodeType() != LayoutNodeTypes.TYPE_BOX_TABLE ) {
return;
}
paginationTableState = new FlowPaginationTableState( paginationTableState );
}
protected void uninstallTableContext( final RenderBox box ) {
if ( box.getNodeType() != LayoutNodeTypes.TYPE_BOX_TABLE ) {
return;
}
paginationTableState = paginationTableState.pop();
}
}