/*
* 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 - 2009 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.layout.model.BlockRenderBox;
import org.pentaho.reporting.engine.classic.core.layout.model.CanvasRenderBox;
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.RenderableReplacedContentBox;
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.IterateStructuralProcessStep;
import org.pentaho.reporting.engine.classic.core.layout.process.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.DebugLog;
import org.pentaho.reporting.libraries.base.util.GenericObjectTable;
/**
* 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
*/
public class TableContentProducer extends IterateStructuralProcessStep
{
private static final Log logger = LogFactory.getLog(TableContentProducer.class);
private SheetLayout sheetLayout;
private GenericObjectTable contentBackend;
private long maximumHeight;
private long maximumWidth;
private TableRectangle lookupRectangle;
private long pageOffset;
private long pageEnd;
private String sheetName;
private int finishedRows;
private int filledRows;
private int clearedRows;
private long contentOffset;
private long effectiveOffset;
private boolean unalignedPagebands;
private boolean headerProcessed;
private boolean ellipseAsBackground;
private boolean shapesAsContent;
private boolean verboseCellMarkers;
private boolean debugReportLayout;
private boolean reportCellConflicts;
private int sectionDepth;
private int 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.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();
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.reportCellConflicts = "true".equals(config.getConfigProperty
("org.pentaho.reporting.engine.classic.core.modules.output.table.base.ReportCellConflicts"));
}
protected void updateSheetLayout(final SheetLayout sheetLayout)
{
if (sheetLayout == null)
{
throw new NullPointerException();
}
this.sheetLayout = sheetLayout;
this.maximumHeight = sheetLayout.getMaxHeight();
this.maximumWidth = sheetLayout.getMaxWidth();
this.contentBackend = new GenericObjectTable(Math.max(1, sheetLayout.getRowCount()), Math.max(1,
sheetLayout.getColumnCount()));
this.contentBackend.ensureCapacity(sheetLayout.getRowCount(), sheetLayout.getColumnCount());
}
public String getSheetName()
{
return sheetName;
}
public void compute(final LogicalPageBox logicalPage,
final boolean iterativeUpdate)
{
// this.iterativeUpdate = iterativeUpdate;
// 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;
pageEnd = logicalPage.getPageEnd() - logicalPage.getPageOffset();
effectiveOffset = 0;
//Log.debug ("Content Processing " + pageOffset + " -> " + pageEnd);
sectionType = CellMarker.TYPE_INVALID;
if (startBlockBox(logicalPage))
{
if (headerProcessed == false)
{
sectionType = CellMarker.TYPE_HEADER;
startProcessing(logicalPage.getWatermarkArea());
final BlockRenderBox headerArea = logicalPage.getHeaderArea();
startProcessing(headerArea);
headerProcessed = true;
}
sectionType = CellMarker.TYPE_NORMALFLOW;
processBoxChilds(logicalPage);
if (iterativeUpdate == false)
{
sectionType = CellMarker.TYPE_FOOTER;
final BlockRenderBox pageFooterBox = logicalPage.getFooterArea();
pageEnd += pageFooterBox.getHeight();
startProcessing(pageFooterBox);
}
}
sectionType = CellMarker.TYPE_INVALID;
finishBlockBox(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);
effectiveOffset = 0;
pageOffset = 0;
pageEnd = logicalPage.getPageEnd();
sectionType = CellMarker.TYPE_INVALID;
if (startBlockBox(logicalPage))
{
if (headerProcessed == false)
{
sectionType = CellMarker.TYPE_HEADER;
contentOffset = 0;
final BlockRenderBox watermarkArea = logicalPage.getWatermarkArea();
pageEnd = watermarkArea.getHeight();
startProcessing(watermarkArea);
final BlockRenderBox headerArea = logicalPage.getHeaderArea();
pageEnd = headerArea.getHeight();
startProcessing(headerArea);
contentOffset = headerArea.getHeight();
headerProcessed = true;
}
final BlockRenderBox headerArea = logicalPage.getHeaderArea();
sectionType = CellMarker.TYPE_NORMALFLOW;
pageOffset = logicalPage.getPageOffset();
pageEnd = logicalPage.getPageEnd();
effectiveOffset = headerArea.getHeight();
processBoxChilds(logicalPage);
if (iterativeUpdate == false)
{
sectionType = CellMarker.TYPE_FOOTER;
pageOffset = 0;
final BlockRenderBox footerArea = logicalPage.getFooterArea();
final long footerOffset = contentOffset + (logicalPage.getPageEnd() - logicalPage.getPageOffset());
pageEnd = footerOffset + footerArea.getHeight();
effectiveOffset = footerOffset;
startProcessing(footerArea);
}
}
sectionType = CellMarker.TYPE_INVALID;
finishBlockBox(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();
}
if (iterativeUpdate == false)
{
headerProcessed = false;
}
}
protected void computeDesigntimeConflicts(final RenderBox box)
{
effectiveOffset = 0;
pageOffset = 0;
pageEnd = box.getHeight();
contentOffset = 0;
contentBackend.clear();
startProcessing(box);
filledRows = getRowCount();
}
public RenderBox getContent(final int row, final int column)
{
if (verboseCellMarkers == false)
{
if (row < finishedRows)
{
return null;
}
}
final CellMarker marker = (CellMarker) contentBackend.getObject(row, column);
if (marker == null)
{
return null;
}
return marker.getContent();
}
public int getSectionType(final int row, final int column)
{
if (verboseCellMarkers == false)
{
if (row < finishedRows)
{
return -1;
}
}
final CellMarker marker = (CellMarker) contentBackend.getObject(row, column);
if (marker == null)
{
return -1;
}
return marker.getSectionType();
}
public long getContentOffset(final int row, final int column)
{
if (verboseCellMarkers == false)
{
if (row < finishedRows)
{
return 0;
}
}
final CellMarker marker = (CellMarker) 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 (box.isFinished())
{
return true;
}
// if (box.isOpen())
// {
// Log.debug("Received open box: " + box);
// }
final long y = effectiveOffset + box.getY() - pageOffset;
final long height = box.getHeight();
final long pageHeight = effectiveOffset + (pageEnd - pageOffset);
// Log.debug ("Processing Box " + effectiveOffset + " " + pageHeight + " -> " + y + " " + height);
// Log.debug ("Processing Box " + box);
//
if (height > 0)
{
if ((y + height) <= effectiveOffset)
{
return false;
}
if (y >= pageHeight)
{
return false;
}
}
else
{
// zero height boxes are always a bit tricky ..
if ((y + height) < effectiveOffset)
{
return false;
}
if (y > pageHeight)
{
return false;
}
}
// Always process everything ..
final long y1 = Math.max(0, 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.setFinished(true);
}
if (box.isFinished())
{
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;
if (verboseCellMarkers)
{
bandMarker = new BandMarker(sectionType, sectionDepth, box.toString());
}
else
{
bandMarker = BandMarker.getMarkerFor(sectionType, sectionDepth);
}
for (int r = Math.max(lookupRectangle.getY1(), finishedRows); r < rectY2; r++)
{
for (int c = lookupRectangle.getX1(); c < rectX2; c++)
{
final CellMarker o = (CellMarker) 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, effectiveOffset - 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.setFinished(true);
}
return true;
}
protected boolean isReplaceableBackground(final CellMarker oldMarker, final CellMarker newMarker)
{
if (oldMarker == null)
{
return true;
}
if (oldMarker.getSectionType() == CellMarker.TYPE_INVALID)
{
return true;
}
if (oldMarker.getSectionDepth() < newMarker.getSectionDepth())
{
return true;
}
return false;
}
protected void handleContentConflict(final RenderBox box)
{
if (reportCellConflicts)
{
TableContentProducer.logger.debug("LayoutShift: Offending Content: " + box);
TableContentProducer.logger.debug("LayoutShift: Offending Content: " + box.isFinished());
}
}
protected void collectSheetStyleData(final RenderBox box)
{
final String sheetName = (String) box.getStyleSheet().getStyleProperty(BandStyleKeys.COMPUTED_SHEETNAME);
if (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)
{
TableContentProducer.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)
{
TableContentProducer.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)
{
TableContentProducer.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 = (CellMarker) contentBackend.getObject(row, column);
if (o == null)
{
if (debugReportLayout)
{
TableContentProducer.logger.debug("maybe Cannot clear row: Cell (" + column + ", " + row + ") is undefined.");
}
lastRowsUndefined = true;
continue;
}
else if (lastRowsUndefined)
{
if (debugReportLayout)
{
TableContentProducer.logger.debug("Cannot clear row: Inner Cell (" + column + ", " + row + ") is undefined.");
}
return;
}
final boolean b = o.isFinished();
if (b == false)
{
if (debugReportLayout)
{
TableContentProducer.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)
{
TableContentProducer.logger.debug("#Cleared row: " + clearRowNr + '.');
}
if (verboseCellMarkers)
{
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);
}
}
lastRowCleared = row;
}
}
if (debugReportLayout)
{
TableContentProducer.logger.debug("Need to clear row: " + (lastRowCleared + 1) + " - " + filledRows);
}
finishedRows = filledRows;
if (atleastOneRowHasContent)
{
for (int clearRowNr = lastRowCleared + 1; clearRowNr < finishedRows; clearRowNr++)
{
if (debugReportLayout)
{
TableContentProducer.logger.debug("*Cleared row: " + clearRowNr + '.');
}
if (verboseCellMarkers)
{
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);
}
clearedRows = clearRowNr;
}
}
}
protected boolean startBlockBox(final BlockRenderBox box)
{
return startBox(box);
}
protected boolean startInlineBox(final InlineRenderBox box)
{
// we should not have come that far ..
return false;
}
protected boolean startOtherBox(final RenderBox box)
{
return startBox(box);
}
public boolean startCanvasBox(final CanvasRenderBox box)
{
return startBox(box);
}
protected boolean startRowBox(final RenderBox box)
{
return startBox(box);
}
protected void endBox(final RenderBox box)
{
sectionDepth -= 1;
}
protected void finishCanvasBox(final CanvasRenderBox box)
{
endBox(box);
}
protected void finishBlockBox(final BlockRenderBox box)
{
endBox(box);
}
protected void finishOtherBox(final RenderBox box)
{
endBox(box);
}
protected void finishRowBox(final RenderBox box)
{
endBox(box);
}
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 = (CellMarker) contentBackend.getObject(row, column);
if (o == null)
{
if (debugReportLayout)
{
TableContentProducer.logger.debug("Row: Cell (" + column + ", " + row + ") is undefined.");
}
lastRowsUndefined = true;
continue;
}
else if (lastRowsUndefined)
{
if (debugReportLayout)
{
TableContentProducer.logger.debug("Row: Inner Cell (" + column + ", " + row + ") is undefined.");
}
return;
}
if (o.isCommited() == false)
{
if (debugReportLayout)
{
TableContentProducer.logger.debug("Row: Cell (" + column + ", " + row + ") is not commited.");
}
return;
}
}
if (debugReportLayout)
{
TableContentProducer.logger.debug("Processable Row: " + filledRows + ".");
}
filledRows = row + 1;
}
if (debugReportLayout)
{
TableContentProducer.logger.debug("Processable Rows: " + finishedRows + ' ' + filledRows + '.');
}
}
protected void processRenderableContent(final RenderableReplacedContentBox box)
{
startBox(box);
endBox(box);
}
public long getContentRowCount()
{
return contentBackend.getRowCount();
}
}