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