1 /*
2  * Licensed to the Apache Software Foundation (ASF) under one or more
3  * contributor license agreements.  See the NOTICE file distributed with
4  * this work for additional information regarding copyright ownership.
5  * The ASF licenses this file to You under the Apache License, Version 2.0
6  * (the "License"); you may not use this file except in compliance with
7  * the License.  You may obtain a copy of the License at
8  *
9  *      http://www.apache.org/licenses/LICENSE-2.0
10  *
11  * Unless required by applicable law or agreed to in writing, software
12  * distributed under the License is distributed on an "AS IS" BASIS,
13  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14  * See the License for the specific language governing permissions and
15  * limitations under the License.
16  */
17 
18 /* $Id: CollapsingBorderResolver.java 1762060 2016-09-23 12:57:46Z ssteiner $ */
19 
20 package org.apache.fop.fo.flow.table;
21 
22 import java.util.ArrayList;
23 import java.util.Iterator;
24 import java.util.List;
25 
26 import org.apache.fop.fo.properties.CommonBorderPaddingBackground;
27 import org.apache.fop.layoutmgr.table.CollapsingBorderModel;
28 
29 /**
30  * A class that implements the border-collapsing model.
31  */
32 class CollapsingBorderResolver implements BorderResolver {
33 
34     private Table table;
35 
36     private CollapsingBorderModel collapsingBorderModel;
37 
38     /**
39      * The previously registered row, either in the header or the body(-ies), but not in
40      * the footer (handled separately).
41      */
42     private List<GridUnit> previousRow;
43 
44     private boolean firstInTable;
45 
46     private List<GridUnit> footerFirstRow;
47 
48     /** The last currently registered footer row. */
49     private List<GridUnit> footerLastRow;
50 
51     private Resolver delegate;
52 
53     // Re-use the same ResolverInBody for every table-body
54     // Important to properly handle firstInBody!!
55     private Resolver resolverInBody = new ResolverInBody();
56 
57     private Resolver resolverInFooter;
58 
59     private List<ConditionalBorder> leadingBorders;
60 
61     private List<ConditionalBorder> trailingBorders;
62 
63     /* TODO Temporary hack for resolved borders in header */
64     /* Currently the normal border is always used. */
65     private List<GridUnit> headerLastRow;
66     /* End of temporary hack */
67 
68     /**
69      * Base class for delegate resolvers. Implementation of the State design pattern: the
70      * treatment differs slightly whether we are in the table's header, footer or body. To
71      * avoid complicated if statements, specialised delegate resolvers will be used
72      * instead.
73      */
74     private abstract class Resolver {
75 
76         protected TablePart tablePart;
77 
78         protected boolean firstInPart;
79 
80         private BorderSpecification borderStartTableAndBody;
81         private BorderSpecification borderEndTableAndBody;
82 
83         /**
84          * Integrates border-before specified on the table and its column.
85          *
86          * @param row the first row of the table (in the header, or in the body if the
87          * table has no header)
88          * @param withNormal
89          * @param withLeadingTrailing
90          * @param withRest
91          */
resolveBordersFirstRowInTable(List<GridUnit> row, boolean withNormal, boolean withLeadingTrailing, boolean withRest)92         void resolveBordersFirstRowInTable(List<GridUnit> row, boolean withNormal,
93                 boolean withLeadingTrailing, boolean withRest) {
94             assert firstInTable;
95             for (int i = 0; i < row.size(); i++) {
96                 TableColumn column = table.getColumn(i);
97                 row.get(i).integrateBorderSegment(
98                         CommonBorderPaddingBackground.BEFORE, column, withNormal,
99                         withLeadingTrailing, withRest);
100             }
101             firstInTable = false;
102         }
103 
104         /**
105          * Resolves border-after for the first row, border-before for the second one.
106          *
107          * @param rowBefore
108          * @param rowAfter
109          */
resolveBordersBetweenRows(List<GridUnit> rowBefore, List<GridUnit> rowAfter)110         void resolveBordersBetweenRows(List<GridUnit> rowBefore, List<GridUnit> rowAfter) {
111             assert rowBefore != null && rowAfter != null;
112             for (int i = 0; i < rowAfter.size(); i++) {
113                 GridUnit gu = rowAfter.get(i);
114                 if (gu.getRowSpanIndex() == 0) {
115                     GridUnit beforeGU = rowBefore.get(i);
116                     gu.resolveBorder(beforeGU, CommonBorderPaddingBackground.BEFORE);
117                 }
118             }
119         }
120 
121         /** Integrates the border-after of the part. */
resolveBordersLastRowInPart(List<GridUnit> row, boolean withNormal, boolean withLeadingTrailing, boolean withRest)122         void resolveBordersLastRowInPart(List<GridUnit> row, boolean withNormal,
123                 boolean withLeadingTrailing, boolean withRest) {
124             for (Object aRow : row) {
125                 ((GridUnit) aRow).integrateBorderSegment(CommonBorderPaddingBackground.AFTER,
126                         tablePart, withNormal, withLeadingTrailing, withRest);
127             }
128         }
129 
130         /**
131          * Integrates border-after specified on the table and its columns.
132          *
133          * @param row the last row of the footer, or of the last body if the table has no
134          * footer
135          * @param withNormal
136          * @param withLeadingTrailing
137          * @param withRest
138          */
resolveBordersLastRowInTable(List<GridUnit> row, boolean withNormal, boolean withLeadingTrailing, boolean withRest)139         void resolveBordersLastRowInTable(List<GridUnit> row, boolean withNormal,
140                 boolean withLeadingTrailing, boolean withRest) {
141             for (int i = 0; i < row.size(); i++) {
142                 TableColumn column = table.getColumn(i);
143                 row.get(i).integrateBorderSegment(CommonBorderPaddingBackground.AFTER,
144                         column, withNormal, withLeadingTrailing, withRest);
145             }
146         }
147 
148         /**
149          * Integrates either border-before specified on the table and its columns if the
150          * table has no header, or border-after specified on the cells of the header's
151          * last row. For the case the grid unit are at the top of a page.
152          *
153          * @param row
154          */
integrateLeadingBorders(List<GridUnit> row)155         void integrateLeadingBorders(List<GridUnit> row) {
156             for (int i = 0; i < table.getNumberOfColumns(); i++) {
157                 GridUnit gu = row.get(i);
158                 ConditionalBorder border = leadingBorders.get(i);
159                 gu.integrateCompetingBorder(CommonBorderPaddingBackground.BEFORE, border,
160                         false, true, true);
161             }
162         }
163 
164         /**
165          * Integrates either border-after specified on the table and its columns if the
166          * table has no footer, or border-before specified on the cells of the footer's
167          * first row. For the case the grid unit are at the bottom of a page.
168          *
169          * @param row
170          */
integrateTrailingBorders(List<GridUnit> row)171         void integrateTrailingBorders(List<GridUnit> row) {
172             for (int i = 0; i < table.getNumberOfColumns(); i++) {
173                 GridUnit gu = row.get(i);
174                 ConditionalBorder border = trailingBorders.get(i);
175                 gu.integrateCompetingBorder(CommonBorderPaddingBackground.AFTER, border,
176                         false, true, true);
177             }
178         }
179 
startPart(TablePart part)180         void startPart(TablePart part) {
181             tablePart = part;
182             firstInPart = true;
183             borderStartTableAndBody = collapsingBorderModel.determineWinner(table.borderStart,
184                     tablePart.borderStart);
185             borderEndTableAndBody = collapsingBorderModel.determineWinner(table.borderEnd,
186                     tablePart.borderEnd);
187         }
188 
189         /**
190          * Resolves the applicable borders for the given row.
191          * <ul>
192          * <li>Integrates the border-before/after of the containing table-row if any;</li>
193          * <li>Integrates the border-before of the containing part, if first row;</li>
194          * <li>Resolves border-start/end between grid units.</li>
195          * </ul>
196          *
197          * @param row the row being finished
198          * @param container the containing element
199          */
endRow(List<GridUnit> row, TableCellContainer container)200         void endRow(List<GridUnit> row, TableCellContainer container) {
201             BorderSpecification borderStart = borderStartTableAndBody;
202             BorderSpecification borderEnd = borderEndTableAndBody;
203             // Resolve before- and after-borders for the table-row
204             if (container instanceof TableRow) {
205                 TableRow tableRow = (TableRow) container;
206                 for (Object aRow : row) {
207                     GridUnit gu = (GridUnit) aRow;
208                     boolean first = (gu.getRowSpanIndex() == 0);
209                     boolean last = gu.isLastGridUnitRowSpan();
210                     gu.integrateBorderSegment(CommonBorderPaddingBackground.BEFORE, tableRow,
211                             first, first, true);
212                     gu.integrateBorderSegment(CommonBorderPaddingBackground.AFTER, tableRow,
213                             last, last, true);
214                 }
215                 borderStart = collapsingBorderModel.determineWinner(borderStart,
216                         tableRow.borderStart);
217                 borderEnd = collapsingBorderModel.determineWinner(borderEnd,
218                         tableRow.borderEnd);
219             }
220             if (firstInPart) {
221                 // Integrate the border-before of the part
222                 for (Object aRow : row) {
223                     ((GridUnit) aRow).integrateBorderSegment(
224                             CommonBorderPaddingBackground.BEFORE, tablePart, true, true, true);
225                 }
226                 firstInPart = false;
227             }
228             // Resolve start/end borders in the row
229             Iterator guIter = row.iterator();
230             GridUnit gu = (GridUnit) guIter.next();
231             Iterator colIter = table.getColumns().iterator();
232             TableColumn col = (TableColumn) colIter.next();
233             gu.integrateBorderSegment(CommonBorderPaddingBackground.START, col);
234             gu.integrateBorderSegment(CommonBorderPaddingBackground.START, borderStart);
235             while (guIter.hasNext()) {
236                 GridUnit nextGU = (GridUnit) guIter.next();
237                 TableColumn nextCol = (TableColumn) colIter.next();
238                 if (gu.isLastGridUnitColSpan()) {
239                     gu.integrateBorderSegment(CommonBorderPaddingBackground.END, col);
240                     nextGU.integrateBorderSegment(CommonBorderPaddingBackground.START, nextCol);
241                     gu.resolveBorder(nextGU, CommonBorderPaddingBackground.END);
242                 }
243                 gu = nextGU;
244                 col = nextCol;
245             }
246             gu.integrateBorderSegment(CommonBorderPaddingBackground.END, col);
247             gu.integrateBorderSegment(CommonBorderPaddingBackground.END, borderEnd);
248         }
249 
endPart()250         void endPart() {
251             resolveBordersLastRowInPart(previousRow, true, true, true);
252         }
253 
endTable()254         abstract void endTable();
255     }
256 
257     private class ResolverInHeader extends Resolver {
258 
endRow(List<GridUnit> row, TableCellContainer container)259         void endRow(List<GridUnit> row, TableCellContainer container) {
260             super.endRow(row, container);
261             if (previousRow != null) {
262                 resolveBordersBetweenRows(previousRow, row);
263             } else {
264                 /*
265                  * This is a bit hacky...
266                  * The two only sensible values for border-before on the header's first row are:
267                  * - at the beginning of the table (normal case)
268                  * - if the header is repeated after each page break
269                  * To represent those values we (ab)use the normal and the rest fields of
270                  * ConditionalBorder. But strictly speaking this is not their purposes.
271                  */
272                 for (Object aRow : row) {
273                     ConditionalBorder borderBefore = ((GridUnit) aRow).borderBefore;
274                     borderBefore.leadingTrailing = borderBefore.normal;
275                     borderBefore.rest = borderBefore.normal;
276                 }
277                 resolveBordersFirstRowInTable(row, true, false, true);
278             }
279             previousRow = row;
280         }
281 
endPart()282         void endPart() {
283             super.endPart();
284             leadingBorders = new ArrayList(table.getNumberOfColumns());
285             /*
286              * Another hack...
287              * The border-after of a header is always the same. Leading and rest don't
288              * apply to cells in the header since they are never broken. To ease
289              * resolution we override the (normally unused) leadingTrailing and rest
290              * fields of ConditionalBorder with the only sensible normal field. That way
291              * grid units from the body will always resolve against the same, normal
292              * header border.
293              */
294             for (Object aPreviousRow : previousRow) {
295                 ConditionalBorder borderAfter = ((GridUnit) aPreviousRow).borderAfter;
296                 borderAfter.leadingTrailing = borderAfter.normal;
297                 borderAfter.rest = borderAfter.normal;
298                 leadingBorders.add(borderAfter);
299             }
300             /* TODO Temporary hack for resolved borders in header */
301             headerLastRow = previousRow;
302             /* End of temporary hack */
303         }
304 
endTable()305         void endTable() {
306             throw new IllegalStateException();
307         }
308     }
309 
310     private class ResolverInFooter extends Resolver {
311 
endRow(List<GridUnit> row, TableCellContainer container)312         void endRow(List<GridUnit> row, TableCellContainer container) {
313             super.endRow(row, container);
314             if (footerFirstRow == null) {
315                 footerFirstRow = row;
316             } else {
317                 // There is a previous row
318                 resolveBordersBetweenRows(footerLastRow, row);
319             }
320             footerLastRow = row;
321         }
322 
endPart()323         void endPart() {
324             resolveBordersLastRowInPart(footerLastRow, true, true, true);
325             trailingBorders = new ArrayList(table.getNumberOfColumns());
326             // See same method in ResolverInHeader for an explanation of the hack
327             for (Object aFooterFirstRow : footerFirstRow) {
328                 ConditionalBorder borderBefore = ((GridUnit) aFooterFirstRow).borderBefore;
329                 borderBefore.leadingTrailing = borderBefore.normal;
330                 borderBefore.rest = borderBefore.normal;
331                 trailingBorders.add(borderBefore);
332             }
333         }
334 
endTable()335         void endTable() {
336             // Resolve after/before border between the last row of table-body and the
337             // first row of table-footer
338             resolveBordersBetweenRows(previousRow, footerFirstRow);
339             // See endRow method in ResolverInHeader for an explanation of the hack
340             for (Object aFooterLastRow : footerLastRow) {
341                 ConditionalBorder borderAfter = ((GridUnit) aFooterLastRow).borderAfter;
342                 borderAfter.leadingTrailing = borderAfter.normal;
343                 borderAfter.rest = borderAfter.normal;
344             }
345             resolveBordersLastRowInTable(footerLastRow, true, false, true);
346         }
347     }
348 
349     private class ResolverInBody extends Resolver {
350 
351         private boolean firstInBody = true;
352 
endRow(List<GridUnit> row, TableCellContainer container)353         void endRow(List<GridUnit> row, TableCellContainer container) {
354             super.endRow(row, container);
355             if (firstInTable) {
356                 resolveBordersFirstRowInTable(row, true, true, true);
357             } else {
358                 // Either there is a header, and then previousRow is set to the header's last row,
359                 // or this is not the first row in the body, and previousRow is not null
360                 resolveBordersBetweenRows(previousRow, row);
361                 integrateLeadingBorders(row);
362             }
363             integrateTrailingBorders(row);
364             previousRow = row;
365             if (firstInBody) {
366                 firstInBody = false;
367                 for (Object aRow : row) {
368                     GridUnit gu = (GridUnit) aRow;
369                     gu.borderBefore.leadingTrailing = gu.borderBefore.normal;
370                 }
371             }
372         }
373 
endTable()374         void endTable() {
375             if (resolverInFooter != null) {
376                 resolverInFooter.endTable();
377             } else {
378                 // Trailing and rest borders already resolved with integrateTrailingBorders
379                 resolveBordersLastRowInTable(previousRow, true, false, false);
380             }
381             for (Object aPreviousRow : previousRow) {
382                 GridUnit gu = (GridUnit) aPreviousRow;
383                 gu.borderAfter.leadingTrailing = gu.borderAfter.normal;
384             }
385         }
386     }
387 
CollapsingBorderResolver(Table table)388     CollapsingBorderResolver(Table table) {
389         this.table = table;
390         collapsingBorderModel = CollapsingBorderModel.getBorderModelFor(table.getBorderCollapse());
391         firstInTable = true;
392         // Resolve before and after borders between the table and each table-column
393         int index = 0;
394         do {
395             TableColumn col = table.getColumn(index);
396             // See endRow method in ResolverInHeader for an explanation of the hack
397             col.borderBefore.integrateSegment(table.borderBefore, true, false, true);
398             col.borderBefore.leadingTrailing = col.borderBefore.rest;
399             col.borderAfter.integrateSegment(table.borderAfter, true, false, true);
400             col.borderAfter.leadingTrailing = col.borderAfter.rest;
401             /*
402              * TODO The border resolution must be done only once for each table column,
403              * even if it's repeated; otherwise, re-resolving against the table's borders
404              * will lead to null border specifications.
405              *
406              * Eventually table columns should probably be cloned instead.
407              */
408             index += col.getNumberColumnsRepeated();
409         } while (index < table.getNumberOfColumns());
410     }
411 
412     /** {@inheritDoc} */
endRow(List<GridUnit> row, TableCellContainer container)413     public void endRow(List<GridUnit> row, TableCellContainer container) {
414         delegate.endRow(row, container);
415     }
416 
417     /** {@inheritDoc} */
startPart(TablePart part)418     public void startPart(TablePart part) {
419         if (part instanceof TableHeader) {
420             delegate = new ResolverInHeader();
421         } else {
422             if (leadingBorders == null || table.omitHeaderAtBreak()) {
423                 // No header, leading borders determined by the table
424                 leadingBorders = new ArrayList(table.getNumberOfColumns());
425                 for (Object o : table.getColumns()) {
426                     ConditionalBorder border = ((TableColumn) o).borderBefore;
427                     leadingBorders.add(border);
428                 }
429             }
430             if (part instanceof TableFooter) {
431                 resolverInFooter = new ResolverInFooter();
432                 delegate = resolverInFooter;
433             } else {
434                 if (trailingBorders == null || table.omitFooterAtBreak()) {
435                     // No footer, trailing borders determined by the table
436                     trailingBorders = new ArrayList(table.getNumberOfColumns());
437                     for (Object o : table.getColumns()) {
438                         ConditionalBorder border = ((TableColumn) o).borderAfter;
439                         trailingBorders.add(border);
440                     }
441                 }
442                 delegate = resolverInBody;
443             }
444         }
445         delegate.startPart(part);
446     }
447 
448     /** {@inheritDoc} */
endPart()449     public void endPart() {
450         delegate.endPart();
451     }
452 
453     /** {@inheritDoc} */
endTable()454     public void endTable() {
455         delegate.endTable();
456         delegate = null;
457         /* TODO Temporary hack for resolved borders in header */
458         if (headerLastRow != null) {
459             for (Object aHeaderLastRow : headerLastRow) {
460                 GridUnit gu = (GridUnit) aHeaderLastRow;
461                 gu.borderAfter.leadingTrailing = gu.borderAfter.normal;
462             }
463         }
464         if (footerLastRow != null) {
465             for (Object aFooterLastRow : footerLastRow) {
466                 GridUnit gu = (GridUnit) aFooterLastRow;
467                 gu.borderAfter.leadingTrailing = gu.borderAfter.normal;
468             }
469         }
470         /* End of temporary hack */
471     }
472 }
473