/* * 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.model; import org.pentaho.reporting.engine.classic.core.ClassicEngineBoot; import org.pentaho.reporting.engine.classic.core.ReportAttributeMap; import org.pentaho.reporting.engine.classic.core.layout.model.context.NodeLayoutProperties; import org.pentaho.reporting.engine.classic.core.metadata.ElementType; import org.pentaho.reporting.engine.classic.core.states.ReportStateKey; import org.pentaho.reporting.engine.classic.core.style.ElementStyleKeys; import org.pentaho.reporting.engine.classic.core.style.StyleSheet; import org.pentaho.reporting.engine.classic.core.style.VerticalTextAlign; import org.pentaho.reporting.engine.classic.core.util.InstanceID; import org.pentaho.reporting.engine.classic.core.util.geom.StrictBounds; import org.pentaho.reporting.libraries.base.config.Configuration; public abstract class RenderNode implements Cloneable { public enum CacheState { CLEAN, DIRTY, DEEP_DIRTY } private static final boolean paranoidModelChecks; static { final Configuration configuration = ClassicEngineBoot.getInstance().getGlobalConfig(); if ( "true".equals( configuration .getConfigProperty( "org.pentaho.reporting.engine.classic.core.layout.ParanoidChecks" ) ) ) { paranoidModelChecks = true; } else { paranoidModelChecks = false; } } public static boolean isParanoidModelChecks() { return paranoidModelChecks; } public static final int HORIZONTAL_AXIS = 0; public static final int VERTICAL_AXIS = 1; public static final CacheState CACHE_CLEAN = CacheState.CLEAN; public static final CacheState CACHE_DIRTY = CacheState.DIRTY; public static final CacheState CACHE_DEEP_DIRTY = CacheState.DEEP_DIRTY; private static final int FLAG_FROZEN = 0x01; private static final int FLAG_FINISHED_PAGINATE = 0x02; private static final int FLAG_FINISHED_TABLE = 0x04; private static final int FLAG_VIRTUAL_NODE = 0x04; private static final int FLAG_WIDOW_BOX = 0x08; private static final int FLAG_RESERVED = 0xFFF0; private int flags; private CacheState cacheState; private CacheState applyState; private RenderBox parentNode; private RenderNode nextNode; private RenderNode prevNode; private NodeLayoutProperties nodeLayoutProperties; private long changeTracker; private long minimumChunkWidth; private long maximumBoxWidth; private long validateModelAge; private ValidationResult validateModelResult; private long linebreakAge; private long cachedX; private long cachedY; private long cachedWidth; private long cachedHeight; private long x; private long y; private long width; private long height; private long cachedAge; protected RenderNode( final int majorAxis, final int minorAxis, final StyleSheet styleSheet, final InstanceID instanceID, final ElementType elementType, final ReportAttributeMap<Object> attributes ) { this( new NodeLayoutProperties( majorAxis, minorAxis, styleSheet, attributes, instanceID, elementType ) ); } protected RenderNode( final NodeLayoutProperties nodeLayoutProperties ) { if ( nodeLayoutProperties == null ) { throw new NullPointerException(); } this.nodeLayoutProperties = nodeLayoutProperties; this.cacheState = RenderNode.CACHE_DIRTY; } protected void reinit( final StyleSheet styleSheet, final ElementType elementType, final ReportAttributeMap<Object> attributes, final InstanceID instanceId ) { if ( attributes == null ) { throw new NullPointerException(); } if ( instanceId == null ) { throw new NullPointerException(); } if ( elementType == null ) { throw new NullPointerException(); } this.flags = 0; this.changeTracker = -1; this.validateModelAge = 0; this.cachedX = 0; this.cachedY = 0; this.cachedWidth = 0; this.cachedHeight = 0; this.x = 0; this.y = 0; this.width = 0; this.height = 0; this.minimumChunkWidth = 0; this.maximumBoxWidth = 0; this.cacheState = RenderNode.CACHE_DIRTY; this.nodeLayoutProperties = new NodeLayoutProperties( this.nodeLayoutProperties.getMajorAxis(), this.nodeLayoutProperties.getMinorAxis(), styleSheet, attributes, instanceId, elementType ); } public ElementType getElementType() { return nodeLayoutProperties.getElementType(); } public ReportAttributeMap<Object> getAttributes() { return nodeLayoutProperties.getAttributes(); } /** * The content-ref-count counts inline-subreports. */ public int getContentRefCount() { return 0; } public int getTableRefCount() { return 0; } public int getDescendantCount() { return 1; } public boolean isSizeSpecifiesBorderBox() { return true; } public abstract int getNodeType(); public int getLayoutNodeType() { return getNodeType(); } public int getMinorAxis() { return this.nodeLayoutProperties.getMinorAxis(); } public int getMajorAxis() { return this.nodeLayoutProperties.getMajorAxis(); } public final NodeLayoutProperties getNodeLayoutProperties() { return nodeLayoutProperties; } public final long getX() { return x; } public final void setX( final long x ) { this.x = x; // this.updateChangeTracker(); } public final long getY() { return y; } public void shift( final long amount ) { this.y += amount; } public void setY( final long y ) { this.y = y; } protected final void updateCacheState( final CacheState state ) { switch ( state ) { case CLEAN: break; case DIRTY: if ( cacheState == RenderNode.CACHE_CLEAN ) { this.cacheState = RenderNode.CACHE_DIRTY; final RenderBox parent = getParent(); if ( parent != null ) { parent.updateCacheState( RenderNode.CACHE_DIRTY ); } } // if cache-state either dirty or deep-dirty, no need to update. break; case DEEP_DIRTY: if ( cacheState == RenderNode.CACHE_CLEAN ) { final RenderBox parent = getParent(); if ( parent != null ) { parent.updateCacheState( RenderNode.CACHE_DEEP_DIRTY ); } } this.cacheState = RenderNode.CACHE_DEEP_DIRTY; break; default: throw new IllegalArgumentException(); } } public final long getWidth() { return width; } public final void setWidth( final long width ) { if ( width < 0 ) { throw new IndexOutOfBoundsException( "Width cannot be negative" ); } this.width = width; this.updateCacheState( RenderNode.CACHE_DIRTY ); // this.updateChangeTracker(); } public final long getHeight() { return height; } public void setHeight( final long height ) { if ( height < 0 ) { throw new IndexOutOfBoundsException( "Height cannot be negative" ); } this.height = height; // this.updateCacheState(RenderNode.CACHE_DIRTY); } public final StyleSheet getStyleSheet() { return nodeLayoutProperties.getStyleSheet(); } public InstanceID getInstanceId() { return nodeLayoutProperties.getInstanceId(); } protected void updateChangeTracker() { changeTracker += 1; if ( cacheState == RenderNode.CACHE_CLEAN ) { cacheState = RenderNode.CACHE_DIRTY; } final RenderBox parent = getParent(); if ( parent != null ) { parent.updateChangeTracker(); } } public final long getChangeTracker() { return changeTracker; } public final RenderBox getParent() { return parentNode; } public RenderBox getLayoutParent() { if ( parentNode != null ) { if ( parentNode.getNodeType() == LayoutNodeTypes.TYPE_BOX_AUTOLAYOUT ) { return parentNode.getLayoutParent(); } } return parentNode; } protected final void setParent( final RenderBox parent ) { if ( isParanoidModelChecks() ) { final RenderNode prev = getPrev(); if ( parent != null && prev == parent ) { throw new IllegalStateException( "Assertation failed: Cannot have a parent that is the same as a silbling." ); } if ( parent == null ) { final RenderNode next = getNext(); if ( next != null ) { throw new NullPointerException(); } if ( prev != null ) { throw new NullPointerException(); } } } this.parentNode = parent; } public final RenderNode getPrev() { return prevNode; } protected final void setPrevUnchecked( final RenderNode prev ) { this.prevNode = prev; } protected final void setPrev( final RenderNode prev ) { this.prevNode = prev; if ( isParanoidModelChecks() && prev != null ) { final RenderBox parent = getParent(); if ( prev == parent ) { throw new IllegalStateException(); } if ( parent != null ) { if ( parent.getFirstChild() == this ) { throw new NullPointerException( "Cannot have a prev node if the parent has me as first child." ); } } } } public final RenderNode getNext() { return nextNode; } protected final void setNextUnchecked( final RenderNode next ) { this.nextNode = next; } protected final void setNext( final RenderNode next ) { this.nextNode = next; if ( isParanoidModelChecks() && next != null ) { final RenderBox parent = getParent(); if ( next == parent ) { throw new IllegalStateException(); } if ( parent != null ) { if ( parent.getLastChild() == this ) { throw new NullPointerException( "Cannot have a next-node, if the parent has me as last child." ); } } } } public LogicalPageBox getLogicalPage() { RenderNode parent = this; while ( parent != null ) { if ( parent.getNodeType() == LayoutNodeTypes.TYPE_BOX_LOGICALPAGE ) { return (LogicalPageBox) parent; } parent = parent.getParent(); } return null; } /** * Clones this node. Be aware that cloning can get you into deep trouble, as the relations this node has may no longer * be valid. * * @return * @noinspection CloneDoesntDeclareCloneNotSupportedException */ public Object clone() { try { return super.clone(); } catch ( final CloneNotSupportedException e ) { // ignored .. throw new IllegalStateException( "Clone failed for some reason." ); } } /** * Derive creates a disconnected node that shares all the properties of the original node. The derived node will no * longer have any parent, silbling, child or any other relationships with other nodes. * * @param deep * @return */ public RenderNode derive( final boolean deep ) { final RenderNode node = (RenderNode) clone(); node.parentNode = null; node.nextNode = null; node.prevNode = null; if ( deep ) { node.cachedAge = this.changeTracker; node.validateModelAge = -1; // todo PRD-4606 node.cacheState = CACHE_DIRTY; } return node; } public RenderNode deriveFrozen( final boolean deep ) { final RenderNode node = (RenderNode) clone(); node.parentNode = null; node.nextNode = null; node.prevNode = null; node.freeze(); return node; } public boolean isFrozen() { return isFlag( FLAG_FROZEN ); } public RenderNode findNodeById( final InstanceID instanceId ) { if ( instanceId == getInstanceId() ) { return this; } return null; } public boolean isOpen() { return false; } public boolean isEmpty() { return false; } public boolean isDiscardable() { return false; } /** * If that method returns true, the element will not be used for rendering. For the purpose of computing sizes or * performing the layouting (in the validate() step), this element will treated as if it is not there. * <p/> * If the element reports itself as non-empty, however, it will affect the margin computation. * * @return */ public boolean isIgnorableForRendering() { return isEmpty(); } public void freeze() { setFlag( FLAG_FROZEN, true ); } public long getMaximumBoxWidth() { return maximumBoxWidth; } public void setMaximumBoxWidth( final long maximumBoxWidth ) { this.maximumBoxWidth = maximumBoxWidth; } public long getMinimumChunkWidth() { return minimumChunkWidth; } protected void setMinimumChunkWidth( final long minimumChunkWidth ) { if ( minimumChunkWidth < 0 ) { throw new IllegalArgumentException(); } this.minimumChunkWidth = minimumChunkWidth; } public long getEffectiveMarginTop() { return 0; } public long getEffectiveMarginBottom() { return 0; } public VerticalTextAlign getVerticalTextAlignment() { return nodeLayoutProperties.getVerticalTextAlign(); } // // /** // * The sticky-Marker contains the original Y of this node. // * @return // */ // public long getStickyMarker() // { // return stickyMarker; // } // // public void setStickyMarker(final long stickyMarker) // { // this.stickyMarker = stickyMarker; // } public String getName() { return null; } public boolean isBreakAfter() { return false; } public long getValidateModelAge() { return validateModelAge; } protected void resetValidateModelResult() { this.validateModelAge = -1; } public void setValidateModelResult( final ValidationResult result ) { this.validateModelAge = changeTracker; this.validateModelResult = result; } public ValidationResult isValidateModelResult() { return validateModelResult; } public long getLinebreakAge() { return linebreakAge; } public void setLinebreakAge( final long linebreakAge ) { this.linebreakAge = linebreakAge; } /** * Returns the cached y position. This position is known after all layouting steps have been finished. In most cases * the layouter tries to reuse the cached values instead of recomputing everything from scratch on each iteration. * <p/> * The cached positions always specify the border-box. If the user specified sizes as content-box sizes, the layouter * converts them into border-box sizes before filling the cache. * * @return the cached x position */ public final long getCachedX() { return cachedX; } /** * Defines the cached x position. This position is known after all layouting steps have been finished. In most cases * the layouter tries to reuse the cached values instead of recomputing everything from scratch on each iteration. * <p/> * The cached positions always specify the border-box. If the user specified sizes as content-box sizes, the layouter * converts them into border-box sizes before filling the cache. * * @param cachedX * the cached x position */ public void setCachedX( final long cachedX ) { this.cachedX = cachedX; } /** * Returns the cached y position. This position is known after all layouting steps have been finished. In most cases * the layouter tries to reuse the cached values instead of recomputing everything from scratch on each iteration. * <p/> * The cached positions always specify the border-box. If the user specified sizes as content-box sizes, the layouter * converts them into border-box sizes before filling the cache. * * @return the cached y position */ public final long getCachedY() { return cachedY; } public final long getCachedY2() { return cachedY + cachedHeight; } /** * Defines the cached y position. This position is known after all layouting steps have been finished. In most cases * the layouter tries to reuse the cached values instead of recomputing everything from scratch on each iteration. * <p/> * The cached positions always specify the border-box. If the user specified sizes as content-box sizes, the layouter * converts them into border-box sizes before filling the cache. * * @param cachedY * the cached y position */ public void setCachedY( final long cachedY ) { this.cachedY = cachedY; } public void shiftCached( final long amount ) { this.cachedY += amount; } public final long getCachedWidth() { return cachedWidth; } public final long getCachedX2() { return cachedX + cachedWidth; } public void setCachedWidth( final long cachedWidth ) { if ( cachedWidth < 0 ) { throw new IndexOutOfBoundsException( "'cached width' cannot be negative." ); } this.cachedWidth = cachedWidth; } public final long getCachedHeight() { return cachedHeight; } public void setCachedHeight( final long cachedHeight ) { if ( cachedHeight < 0 ) { throw new IndexOutOfBoundsException( "'cached height' cannot be negative, was " + cachedHeight ); } this.cachedHeight = cachedHeight; } public void apply() { this.x = this.cachedX; this.y = this.cachedY; this.width = this.cachedWidth; this.height = this.cachedHeight; this.cachedAge = this.changeTracker; this.cacheState = CacheState.CLEAN; this.applyState = CacheState.CLEAN; final RenderBox parent = getParent(); if ( parent != null ) { parent.addOverflowArea( x + getOverflowAreaWidth() - parent.getX(), y + getOverflowAreaHeight() - parent.getY() ); } } public final boolean isLinebreakCacheValid() { if ( linebreakAge != changeTracker ) { return false; } return true; } public final boolean isValidateModelCacheValid() { if ( validateModelAge != changeTracker ) { return false; } if ( validateModelResult == ValidationResult.UNKNOWN ) { return false; } return true; } /** * Checks whether this node can be removed. This flag is used by iterative streaming output targets to mark nodes that * have been fully processed. * * @return */ public boolean isFinishedPaginate() { return isFlag( FLAG_FINISHED_PAGINATE ); } public void setFinishedPaginate( final boolean finished ) { if ( isFinishedPaginate() == true && finished == false ) { throw new IllegalStateException( "Cannot undo a finished-marker" ); } setFlag( FLAG_FINISHED_PAGINATE, finished ); } public boolean isFinishedTable() { return isFlag( FLAG_FINISHED_TABLE ); } public void setFinishedTable( final boolean finished ) { if ( isFinishedTable() == true && finished == false ) { throw new IllegalStateException( "Cannot undo a finished-marker" ); } setFlag( FLAG_FINISHED_TABLE, finished ); } public boolean isDeepFinishedTable() { return isFinishedTable(); } public CacheState getCacheState() { return cacheState; } public ReportStateKey getStateKey() { return null; } public boolean isBoxOverflowX() { return false; } public boolean isBoxOverflowY() { return false; } public final boolean isNodeVisible( final StrictBounds drawArea, final boolean overflowX, final boolean overflowY ) { final long drawAreaX0 = drawArea.getX(); final long drawAreaY0 = drawArea.getY(); return isNodeVisible( drawAreaX0, drawAreaY0, drawArea.getWidth(), drawArea.getHeight(), overflowX, overflowY ); } public final boolean isNodeVisible( final StrictBounds drawArea ) { final long drawAreaX0 = drawArea.getX(); final long drawAreaY0 = drawArea.getY(); return isNodeVisible( drawAreaX0, drawAreaY0, drawArea.getWidth(), drawArea.getHeight() ); } public final boolean isNodeVisible( final long drawAreaX0, final long drawAreaY0, final long drawAreaWidth, final long drawAreaHeight ) { return isNodeVisible( drawAreaX0, drawAreaY0, drawAreaWidth, drawAreaHeight, isBoxOverflowX(), isBoxOverflowY() ); } public final boolean isNodeVisible( final long drawAreaX0, final long drawAreaY0, final long drawAreaWidth, final long drawAreaHeight, final boolean overflowX, final boolean overflowY ) { if ( getStyleSheet().getBooleanStyleProperty( ElementStyleKeys.VISIBLE ) == false ) { return false; } final long drawAreaX1 = drawAreaX0 + drawAreaWidth; final long drawAreaY1 = drawAreaY0 + drawAreaHeight; final long x2 = x + width; final long y2 = y + height; if ( width == 0 ) { if ( x2 < drawAreaX0 ) { return false; } if ( x > drawAreaX1 ) { return false; } } else if ( overflowX == false ) { if ( x2 <= drawAreaX0 ) { return false; } if ( x >= drawAreaX1 ) { return false; } } if ( height == 0 ) { if ( y2 < drawAreaY0 ) { return false; } if ( y > drawAreaY1 ) { return false; } } else if ( overflowY == false ) { if ( y2 <= drawAreaY0 ) { return false; } if ( y >= drawAreaY1 ) { return false; } } return true; } public boolean isVirtualNode() { return isFlag( FLAG_VIRTUAL_NODE ); } public void setVirtualNode( final boolean virtualNode ) { setFlag( FLAG_VIRTUAL_NODE, virtualNode ); } protected void setFlag( final int flag, final boolean value ) { if ( value ) { flags = flags | flag; } else { flags = flags & ( ~flag ); } } protected boolean isFlag( final int flag ) { return ( flags & flag ) != 0; } public final boolean isBoxVisible( final StrictBounds drawArea ) { return isBoxVisible( drawArea.getX(), drawArea.getY(), drawArea.getWidth(), drawArea.getHeight() ); } public final boolean isBoxVisible( final long x, final long y, final long width, final long height ) { if ( isNodeVisible( x, y, width, height ) == false ) { return false; } final RenderBox parent = getParent(); if ( parent == null ) { return true; } final StyleSheet styleSheet = getStyleSheet(); if ( styleSheet.getStyleProperty( ElementStyleKeys.ANCHOR_NAME ) != null ) { return true; } if ( parent.getNodeType() != LayoutNodeTypes.TYPE_BOX_AUTOLAYOUT && parent.getStaticBoxLayoutProperties().isOverflowX() == false ) { final long parentX1 = parent.getX(); final long parentX2 = parentX1 + parent.getWidth(); if ( getWidth() == 0 ) { // could be a line .. return true; } final long boxX1 = getX(); final long boxX2 = boxX1 + getWidth(); if ( boxX2 <= parentX1 ) { return false; } if ( boxX1 >= parentX2 ) { return false; } } final int layoutNodeType = getLayoutNodeType(); if ( layoutNodeType == LayoutNodeTypes.TYPE_BOX_TABLE_CELL || layoutNodeType == LayoutNodeTypes.TYPE_BOX_TABLE_ROW ) { // we cannot perform an overflow test for table-rows or table-cells based on the parent node. // With col and row-spanning, this can be non-deterministic. return true; } if ( parent.getNodeType() != LayoutNodeTypes.TYPE_BOX_AUTOLAYOUT && parent.getStaticBoxLayoutProperties().isOverflowY() == false ) { // Compute whether the box is at least partially contained in the parent's bounding box. final long parentY1 = parent.getY(); final long parentY2 = parentY1 + parent.getHeight(); if ( getHeight() == 0 ) { // could be a line .. return true; } final long boxY1 = getY(); final long boxY2 = boxY1 + getHeight(); if ( boxY2 <= parentY1 ) { return false; } if ( boxY1 >= parentY2 ) { return false; } } return true; } public long getOverflowAreaHeight() { return getHeight(); } public long getOverflowAreaWidth() { return getWidth(); } public long getEffectiveMinimumChunkSize() { return minimumChunkWidth; } public int getChildCount() { return 0; } public boolean isWidowBox() { return isFlag( FLAG_WIDOW_BOX ); } public void setWidowBox( final boolean widowBox ) { setFlag( FLAG_WIDOW_BOX, widowBox ); } public boolean isOrphanLeaf() { return false; } public RenderBox.RestrictFinishClearOut getRestrictFinishedClearOut() { return RenderBox.RestrictFinishClearOut.UNRESTRICTED; } public long getCachedAge() { return cachedAge; } public boolean isCacheValid() { if ( cachedAge != changeTracker ) { return false; } if ( this.cacheState != CACHE_CLEAN ) { return false; } return true; } protected final void setCachedAge( final long cachedAge ) { this.cachedAge = cachedAge; } public final long getY2() { return y + height; } public boolean isVisible() { return nodeLayoutProperties.isVisible(); } public boolean isContainsReservedContent() { return false; } public void markApplyStateDirty() { if ( applyState != CacheState.CLEAN ) { return; } applyState = CACHE_DIRTY; RenderBox parent = getParent(); if ( parent != null ) { parent.markApplyStateDirty(); } } public CacheState getApplyState() { return applyState; } public int getRowIndex() { return 0; } public boolean isRenderBox() { return false; } public int getWidowLeafCount() { return 0; } public int getOrphanLeafCount() { return 0; } }