/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License.  You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

/* $Id: TableContentLayoutManager.java 1761020 2016-09-16 11:17:35Z ssteiner $ */

package org.apache.fop.layoutmgr.table;

import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.ListIterator;
import java.util.Map;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

import org.apache.fop.datatypes.PercentBaseContext;
import org.apache.fop.fo.Constants;
import org.apache.fop.fo.FObj;
import org.apache.fop.fo.flow.Marker;
import org.apache.fop.fo.flow.table.EffRow;
import org.apache.fop.fo.flow.table.PrimaryGridUnit;
import org.apache.fop.fo.flow.table.Table;
import org.apache.fop.fo.flow.table.TableBody;
import org.apache.fop.fo.flow.table.TablePart;
import org.apache.fop.layoutmgr.BreakElement;
import org.apache.fop.layoutmgr.ElementListUtils;
import org.apache.fop.layoutmgr.FootenoteUtil;
import org.apache.fop.layoutmgr.FootnoteBodyLayoutManager;
import org.apache.fop.layoutmgr.Keep;
import org.apache.fop.layoutmgr.KnuthBlockBox;
import org.apache.fop.layoutmgr.KnuthBox;
import org.apache.fop.layoutmgr.KnuthElement;
import org.apache.fop.layoutmgr.KnuthGlue;
import org.apache.fop.layoutmgr.KnuthPossPosIter;
import org.apache.fop.layoutmgr.LayoutContext;
import org.apache.fop.layoutmgr.ListElement;
import org.apache.fop.layoutmgr.PageBreaker;
import org.apache.fop.layoutmgr.Position;
import org.apache.fop.layoutmgr.PositionIterator;
import org.apache.fop.layoutmgr.SpaceResolver.SpaceHandlingBreakPosition;
import org.apache.fop.util.BreakUtil;

Layout manager for table contents, particularly managing the creation of combined element lists.
/** * Layout manager for table contents, particularly managing the creation of combined element lists. */
public class TableContentLayoutManager implements PercentBaseContext {
Logger
/** Logger **/
private static final Log LOG = LogFactory.getLog(TableContentLayoutManager.class); private TableLayoutManager tableLM; private TableRowIterator bodyIter; private TableRowIterator headerIter; private TableRowIterator footerIter; private LinkedList headerList; private LinkedList footerList; private int headerNetHeight; private int footerNetHeight; private int startXOffset; private int usedBPD; private TableStepper stepper; private boolean headerIsBeingRepeated; private boolean atLeastOnce;
Main constructor
Params:
  • parent – Parent layout manager
/** * Main constructor * @param parent Parent layout manager */
TableContentLayoutManager(TableLayoutManager parent) { this.tableLM = parent; Table table = getTableLM().getTable(); this.bodyIter = new TableRowIterator(table, TableRowIterator.BODY); if (table.getTableHeader() != null) { headerIter = new TableRowIterator(table, TableRowIterator.HEADER); } if (table.getTableFooter() != null) { footerIter = new TableRowIterator(table, TableRowIterator.FOOTER); } stepper = new TableStepper(this); }
Returns:the table layout manager
/** * @return the table layout manager */
TableLayoutManager getTableLM() { return this.tableLM; }
Returns:true if the table uses the separate border model.
/** @return true if the table uses the separate border model. */
boolean isSeparateBorderModel() { return getTableLM().getTable().isSeparateBorderModel(); }
Returns:the column setup of this table
/** * @return the column setup of this table */
ColumnSetup getColumns() { return getTableLM().getColumns(); }
Returns:the net header height
/** @return the net header height */
protected int getHeaderNetHeight() { return this.headerNetHeight; }
Returns:the net footer height
/** @return the net footer height */
protected int getFooterNetHeight() { return this.footerNetHeight; }
Returns:the header element list
/** @return the header element list */
protected LinkedList getHeaderElements() { return this.headerList; }
Returns:the footer element list
/** @return the footer element list */
protected LinkedList getFooterElements() { return this.footerList; }
Get a sequence of KnuthElements representing the content of the node assigned to the LM.
Params:
  • context – the LayoutContext used to store layout information
  • alignment – the desired text alignment
See Also:
Returns: the list of KnuthElements
/** * Get a sequence of KnuthElements representing the content * of the node assigned to the LM. * * @param context the LayoutContext used to store layout information * @param alignment the desired text alignment * @return the list of KnuthElements * @see org.apache.fop.layoutmgr.LayoutManager#getNextKnuthElements(LayoutContext, int) */
public List getNextKnuthElements(LayoutContext context, int alignment) { if (LOG.isDebugEnabled()) { LOG.debug("==> Columns: " + getTableLM().getColumns()); } KnuthBox headerAsFirst = null; KnuthBox headerAsSecondToLast = null; KnuthBox footerAsLast = null; LinkedList returnList = new LinkedList(); int headerFootnoteBPD = 0; if (headerIter != null && headerList == null) { this.headerList = getKnuthElementsForRowIterator( headerIter, context, alignment, TableRowIterator.HEADER); this.headerNetHeight = ElementListUtils.calcContentLength(this.headerList); if (LOG.isDebugEnabled()) { LOG.debug("==> Header: " + headerNetHeight + " - " + this.headerList); } TableHeaderFooterPosition pos = new TableHeaderFooterPosition( getTableLM(), true, this.headerList); List<FootnoteBodyLayoutManager> footnoteList = FootenoteUtil.getFootnotes(headerList); KnuthBox box = (footnoteList.isEmpty() || !getTableLM().getTable().omitHeaderAtBreak()) ? new KnuthBox(headerNetHeight, pos, false) : new KnuthBlockBox(headerNetHeight, footnoteList, pos, false); if (getTableLM().getTable().omitHeaderAtBreak()) { //We can simply add the table header at the start //of the whole list headerAsFirst = box; } else { if (!footnoteList.isEmpty()) { List<List<KnuthElement>> footnotes = PageBreaker.getFootnoteKnuthElements( getTableLM().getPSLM().getFlowLayoutManager(), context, footnoteList); getTableLM().setHeaderFootnotes(footnotes); headerFootnoteBPD = getFootnotesBPD(footnotes); returnList.add(new KnuthBlockBox(-headerFootnoteBPD, footnoteList, new Position(getTableLM()), true)); headerNetHeight += headerFootnoteBPD; } headerAsSecondToLast = box; } } if (footerIter != null && footerList == null) { this.footerList = getKnuthElementsForRowIterator( footerIter, context, alignment, TableRowIterator.FOOTER); this.footerNetHeight = ElementListUtils.calcContentLength(this.footerList); if (LOG.isDebugEnabled()) { LOG.debug("==> Footer: " + footerNetHeight + " - " + this.footerList); } //We can simply add the table footer at the end of the whole list TableHeaderFooterPosition pos = new TableHeaderFooterPosition( getTableLM(), false, this.footerList); List<FootnoteBodyLayoutManager> footnoteList = FootenoteUtil.getFootnotes(footerList); footerAsLast = footnoteList.isEmpty() ? new KnuthBox(footerNetHeight, pos, false) : new KnuthBlockBox(footerNetHeight, footnoteList, pos, false); if (!(getTableLM().getTable().omitFooterAtBreak() || footnoteList.isEmpty())) { List<List<KnuthElement>> footnotes = PageBreaker.getFootnoteKnuthElements( getTableLM().getPSLM().getFlowLayoutManager(), context, footnoteList); getTableLM().setFooterFootnotes(footnotes); footerNetHeight += getFootnotesBPD(footnotes); } } returnList.addAll(getKnuthElementsForRowIterator( bodyIter, context, alignment, TableRowIterator.BODY)); if (headerAsFirst != null) { int insertionPoint = 0; if (returnList.size() > 0 && ((ListElement)returnList.getFirst()).isForcedBreak()) { insertionPoint++; } returnList.add(insertionPoint, headerAsFirst); } else if (headerAsSecondToLast != null) { int insertionPoint = returnList.size(); if (returnList.size() > 0 && ((ListElement)returnList.getLast()).isForcedBreak()) { insertionPoint--; } returnList.add(insertionPoint, headerAsSecondToLast); } if (footerAsLast != null) { int insertionPoint = returnList.size(); if (returnList.size() > 0 && ((ListElement)returnList.getLast()).isForcedBreak()) { insertionPoint--; } returnList.add(insertionPoint, footerAsLast); } if (headerFootnoteBPD != 0) { returnList.add(new KnuthBox(headerFootnoteBPD, new Position(getTableLM()), true)); } return returnList; } private int getFootnotesBPD(List<List<KnuthElement>> footnotes) { int bpd = 0; for (List<KnuthElement> footnote : footnotes) { bpd += ElementListUtils.calcContentLength(footnote); } return bpd; }
Creates Knuth elements by iterating over a TableRowIterator.
Params:
  • iter – TableRowIterator instance to fetch rows from
  • context – Active LayoutContext
  • alignment – alignment indicator
  • bodyType – Indicates what kind of body is being processed (BODY, HEADER or FOOTER)
Returns:An element list
/** * Creates Knuth elements by iterating over a TableRowIterator. * @param iter TableRowIterator instance to fetch rows from * @param context Active LayoutContext * @param alignment alignment indicator * @param bodyType Indicates what kind of body is being processed * (BODY, HEADER or FOOTER) * @return An element list */
private LinkedList getKnuthElementsForRowIterator(TableRowIterator iter, LayoutContext context, int alignment, int bodyType) { LinkedList returnList = new LinkedList(); EffRow[] rowGroup = iter.getNextRowGroup(); // TODO homogenize the handling of keeps and breaks context.clearKeepsPending(); context.setBreakBefore(Constants.EN_AUTO); context.setBreakAfter(Constants.EN_AUTO); Keep keepWithPrevious = Keep.KEEP_AUTO; int breakBefore = Constants.EN_AUTO; if (rowGroup != null) { RowGroupLayoutManager rowGroupLM = new RowGroupLayoutManager(getTableLM(), rowGroup, stepper); List nextRowGroupElems = rowGroupLM.getNextKnuthElements(context, alignment, bodyType); keepWithPrevious = keepWithPrevious.compare(context.getKeepWithPreviousPending()); breakBefore = context.getBreakBefore(); int breakBetween = context.getBreakAfter(); returnList.addAll(nextRowGroupElems); while ((rowGroup = iter.getNextRowGroup()) != null) { rowGroupLM = new RowGroupLayoutManager(getTableLM(), rowGroup, stepper); //Note previous pending keep-with-next and clear the strength //(as the layout context is reused) Keep keepWithNextPending = context.getKeepWithNextPending(); context.clearKeepWithNextPending(); //Get elements for next row group nextRowGroupElems = rowGroupLM.getNextKnuthElements(context, alignment, bodyType); /* * The last break element produced by TableStepper (for the previous row * group) may be used to represent the break between the two row groups. * Its penalty value and break class must just be overridden by the * characteristics of the keep or break between the two. * * However, we mustn't forget that if the after border of the last row of * the row group is thicker in the normal case than in the trailing case, * an additional glue will be appended to the element list. So we may have * to go two steps backwards in the list. */ //Determine keep constraints Keep keep = keepWithNextPending.compare(context.getKeepWithPreviousPending()); context.clearKeepWithPreviousPending(); keep = keep.compare(getTableLM().getKeepTogether()); int penaltyValue = keep.getPenalty(); int breakClass = keep.getContext(); breakBetween = BreakUtil.compareBreakClasses(breakBetween, context.getBreakBefore()); if (breakBetween != Constants.EN_AUTO) { penaltyValue = -KnuthElement.INFINITE; breakClass = breakBetween; } BreakElement breakElement; ListIterator elemIter = returnList.listIterator(returnList.size()); ListElement elem = (ListElement) elemIter.previous(); if (elem instanceof KnuthGlue) { breakElement = (BreakElement) elemIter.previous(); } else { breakElement = (BreakElement) elem; } breakElement.setPenaltyValue(penaltyValue); breakElement.setBreakClass(breakClass); returnList.addAll(nextRowGroupElems); breakBetween = context.getBreakAfter(); } } /* * The last break produced for the last row-group of this table part must be * removed, because the breaking after the table will be handled by TableLM. * Unless the element list ends with a glue, which must be kept to accurately * represent the content. In such a case the break is simply disabled by setting * its penalty to infinite. */ ListIterator elemIter = returnList.listIterator(returnList.size()); ListElement elem = (ListElement) elemIter.previous(); if (elem instanceof KnuthGlue) { BreakElement breakElement = (BreakElement) elemIter.previous(); breakElement.setPenaltyValue(KnuthElement.INFINITE); } else { elemIter.remove(); } context.updateKeepWithPreviousPending(keepWithPrevious); context.setBreakBefore(breakBefore); //fox:widow-content-limit int widowContentLimit = getTableLM().getTable().getWidowContentLimit().getValue(); if (widowContentLimit != 0 && bodyType == TableRowIterator.BODY) { ElementListUtils.removeLegalBreaks(returnList, widowContentLimit); } //fox:orphan-content-limit int orphanContentLimit = getTableLM().getTable().getOrphanContentLimit().getValue(); if (orphanContentLimit != 0 && bodyType == TableRowIterator.BODY) { ElementListUtils.removeLegalBreaksFromEnd(returnList, orphanContentLimit); } return returnList; }
Returns the X offset of the given grid unit.
Params:
  • gu – the grid unit
Returns:the requested X offset
/** * Returns the X offset of the given grid unit. * @param gu the grid unit * @return the requested X offset */
protected int getXOffsetOfGridUnit(PrimaryGridUnit gu) { return getXOffsetOfGridUnit(gu.getColIndex(), gu.getCell().getNumberColumnsSpanned()); }
Returns the X offset of the grid unit in the given column.
Params:
  • colIndex – the column index (zero-based)
  • nrColSpan – number columns spanned
Returns:the requested X offset
/** * Returns the X offset of the grid unit in the given column. * @param colIndex the column index (zero-based) * @param nrColSpan number columns spanned * @return the requested X offset */
protected int getXOffsetOfGridUnit(int colIndex, int nrColSpan) { return startXOffset + getTableLM().getColumns().getXOffset(colIndex + 1, nrColSpan, getTableLM()); }
Adds the areas generated by this layout manager to the area tree.
Params:
  • parentIter – the position iterator
  • layoutContext – the layout context for adding areas
/** * Adds the areas generated by this layout manager to the area tree. * @param parentIter the position iterator * @param layoutContext the layout context for adding areas */
void addAreas(PositionIterator parentIter, LayoutContext layoutContext) { this.usedBPD = 0; RowPainter painter = new RowPainter(this, layoutContext); List tablePositions = new java.util.ArrayList(); List headerElements = null; List footerElements = null; Position firstPos = null; Position lastPos = null; Position lastCheckPos = null; while (parentIter.hasNext()) { Position pos = parentIter.next(); if (pos instanceof SpaceHandlingBreakPosition) { //This position has only been needed before addAreas was called, now we need the //original one created by the layout manager. pos = ((SpaceHandlingBreakPosition)pos).getOriginalBreakPosition(); } if (pos == null) { continue; } if (firstPos == null) { firstPos = pos; } lastPos = pos; if (pos.getIndex() >= 0) { lastCheckPos = pos; } if (pos instanceof TableHeaderFooterPosition) { TableHeaderFooterPosition thfpos = (TableHeaderFooterPosition)pos; //these positions need to be unpacked if (thfpos.header) { //Positions for header will be added first headerElements = thfpos.nestedElements; } else { //Positions for footers are simply added at the end footerElements = thfpos.nestedElements; } } else if (pos instanceof TableHFPenaltyPosition) { //ignore for now, see special handling below if break is at a penalty //Only if the last position in this part/page us such a position it will be used } else if (pos instanceof TableContentPosition) { tablePositions.add(pos); } else { if (LOG.isDebugEnabled()) { LOG.debug("Ignoring position: " + pos); } } } boolean treatFooterAsArtifact = layoutContext.treatAsArtifact(); if (lastPos instanceof TableHFPenaltyPosition) { TableHFPenaltyPosition penaltyPos = (TableHFPenaltyPosition)lastPos; LOG.debug("Break at penalty!"); if (penaltyPos.headerElements != null) { //Header positions for the penalty position are in the last element and need to //be handled first before all other TableContentPositions headerElements = penaltyPos.headerElements; } if (penaltyPos.footerElements != null) { footerElements = penaltyPos.footerElements; treatFooterAsArtifact = true; } } // there may be table fragment markers stored; clear them since we are starting a new fragment tableLM.clearTableFragmentMarkers(); // note: markers at table level are to be retrieved by the page, not by the table itself Map<String, Marker> markers = getTableLM().getTable().getMarkers(); if (markers != null) { getTableLM().getCurrentPV().registerMarkers(markers, true, getTableLM().isFirst(firstPos), getTableLM().isLast(lastCheckPos)); } if (headerElements != null) { boolean ancestorTreatAsArtifact = layoutContext.treatAsArtifact(); if (headerIsBeingRepeated) { layoutContext.setTreatAsArtifact(true); if (!getTableLM().getHeaderFootnotes().isEmpty()) { getTableLM().getPSLM().addTableHeaderFootnotes(getTableLM().getHeaderFootnotes()); } } //header positions for the last part are the second-to-last element and need to //be handled first before all other TableContentPositions addHeaderFooterAreas(headerElements, tableLM.getTable().getTableHeader(), painter, false); if (!ancestorTreatAsArtifact) { headerIsBeingRepeated = true; } layoutContext.setTreatAsArtifact(ancestorTreatAsArtifact); } if (tablePositions.isEmpty()) { // TODO make sure this actually never happens LOG.error("tablePositions empty." + " Please send your FO file to fop-users@xmlgraphics.apache.org"); } else { // Here we are sure that posIter iterates only over TableContentPosition instances addBodyAreas(tablePositions.iterator(), painter, footerElements == null); } // if there are TCLMs saved because they have a RetrieveTableMarker, we repeat the header areas now; // this can also be done after the areas for the footer are added but should be the same as here tableLM.setRepeateHeader(atLeastOnce); tableLM.repeatAddAreasForSavedTableHeaderTableCellLayoutManagers(); atLeastOnce = true; if (footerElements != null) { boolean ancestorTreatAsArtifact = layoutContext.treatAsArtifact(); layoutContext.setTreatAsArtifact(treatFooterAsArtifact); //Positions for footers are simply added at the end addHeaderFooterAreas(footerElements, tableLM.getTable().getTableFooter(), painter, true); if (lastPos instanceof TableHFPenaltyPosition && !tableLM.getFooterFootnotes().isEmpty()) { tableLM.getPSLM().addTableFooterFootnotes(getTableLM().getFooterFootnotes()); } layoutContext.setTreatAsArtifact(ancestorTreatAsArtifact); } this.usedBPD += painter.getAccumulatedBPD(); if (markers != null) { getTableLM().getCurrentPV().registerMarkers(markers, false, getTableLM().isFirst(firstPos), getTableLM().isLast(lastCheckPos)); } } private void addHeaderFooterAreas(List elements, TablePart part, RowPainter painter, boolean lastOnPage) { List lst = new java.util.ArrayList(elements.size()); for (Iterator iter = new KnuthPossPosIter(elements); iter.hasNext();) { Position pos = (Position) iter.next(); /* * Unlike for the body the Positions associated to the glues generated by * TableStepper haven't been removed yet. */ if (pos instanceof TableContentPosition) { lst.add((TableContentPosition) pos); } } addTablePartAreas(lst, painter, part, true, true, true, lastOnPage); }
Iterates over the positions corresponding to the table's body (which may contain several table-body elements!) and adds the corresponding areas.
Params:
  • iterator – iterator over TableContentPosition elements. Those positions correspond to the elements of the body present on the current page
  • painter –
  • lastOnPage – true if the table has no footer (then the last line of the table that will be present on the page belongs to the body)
/** * Iterates over the positions corresponding to the table's body (which may contain * several table-body elements!) and adds the corresponding areas. * * @param iterator iterator over TableContentPosition elements. Those positions * correspond to the elements of the body present on the current page * @param painter * @param lastOnPage true if the table has no footer (then the last line of the table * that will be present on the page belongs to the body) */
private void addBodyAreas(Iterator iterator, RowPainter painter, boolean lastOnPage) { painter.startBody(); List lst = new java.util.ArrayList(); TableContentPosition pos = (TableContentPosition) iterator.next(); boolean isFirstPos = pos.getFlag(TableContentPosition.FIRST_IN_ROWGROUP) && pos.getRow().getFlag(EffRow.FIRST_IN_PART); TablePart part = pos.getTablePart(); lst.add(pos); while (iterator.hasNext()) { pos = (TableContentPosition) iterator.next(); if (pos.getTablePart() != part) { addTablePartAreas(lst, painter, part, isFirstPos, true, false, false); isFirstPos = true; lst.clear(); part = pos.getTablePart(); } lst.add(pos); } boolean isLastPos = pos.getFlag(TableContentPosition.LAST_IN_ROWGROUP) && pos.getRow().getFlag(EffRow.LAST_IN_PART); addTablePartAreas(lst, painter, part, isFirstPos, isLastPos, true, lastOnPage); painter.endBody(); }
Adds the areas corresponding to a single fo:table-header/footer/body element.
/** * Adds the areas corresponding to a single fo:table-header/footer/body element. */
private void addTablePartAreas(List positions, RowPainter painter, TablePart body, boolean isFirstPos, boolean isLastPos, boolean lastInBody, boolean lastOnPage) { getTableLM().getCurrentPV().registerMarkers(body.getMarkers(), true, isFirstPos, isLastPos); if (body instanceof TableBody) { getTableLM().registerMarkers(body.getMarkers(), true, isFirstPos, isLastPos); } painter.startTablePart(body); for (Object position : positions) { painter.handleTableContentPosition((TableContentPosition) position); } getTableLM().getCurrentPV().registerMarkers(body.getMarkers(), false, isFirstPos, isLastPos); if (body instanceof TableBody) { getTableLM().registerMarkers(body.getMarkers(), false, isFirstPos, isLastPos); } painter.endTablePart(lastInBody, lastOnPage); }
Sets the overall starting x-offset. Used for proper placement of cells.
Params:
  • startXOffset – starting x-offset (table's start-indent)
/** * Sets the overall starting x-offset. Used for proper placement of cells. * @param startXOffset starting x-offset (table's start-indent) */
void setStartXOffset(int startXOffset) { this.startXOffset = startXOffset; }
Returns:the amount of block-progression-dimension used by the content
/** * @return the amount of block-progression-dimension used by the content */
int getUsedBPD() { return this.usedBPD; } // --------- Property Resolution related functions --------- //
{@inheritDoc}
/** * {@inheritDoc} */
public int getBaseLength(int lengthBase, FObj fobj) { return tableLM.getBaseLength(lengthBase, fobj); } }