001 /* 002 * Copyright (c) 2006 Henri Sivonen 003 * 004 * Permission is hereby granted, free of charge, to any person obtaining a 005 * copy of this software and associated documentation files (the "Software"), 006 * to deal in the Software without restriction, including without limitation 007 * the rights to use, copy, modify, merge, publish, distribute, sublicense, 008 * and/or sell copies of the Software, and to permit persons to whom the 009 * Software is furnished to do so, subject to the following conditions: 010 * 011 * The above copyright notice and this permission notice shall be included in 012 * all copies or substantial portions of the Software. 013 * 014 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 015 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 016 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL 017 * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 018 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 019 * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 020 * DEALINGS IN THE SOFTWARE. 021 */ 022 023 package org.whattf.checker.table; 024 025 import java.util.HashSet; 026 import java.util.Iterator; 027 import java.util.LinkedList; 028 import java.util.List; 029 import java.util.Set; 030 031 import org.whattf.checker.AttributeUtil; 032 import org.xml.sax.Attributes; 033 import org.xml.sax.Locator; 034 import org.xml.sax.SAXException; 035 import org.xml.sax.SAXParseException; 036 import org.xml.sax.helpers.LocatorImpl; 037 038 039 /** 040 * Represents an XHTML table for table integrity checking. Handles 041 * table-significant parse events and keeps track of columns. 042 * 043 * @version $Id: Table.java 205 2007-10-14 19:43:03Z hsivonen $ 044 * @author hsivonen 045 */ 046 final class Table { 047 048 /** 049 * An enumeration for keeping track of the parsing state of a table. 050 */ 051 private enum State { 052 053 /** 054 * The table element start has been seen. No child elements have been seen. 055 * A start of a column, a column group, a row or a row group or the end of 056 * the table is expected. 057 */ 058 IN_TABLE_AT_START, 059 060 /** 061 * The table element is the open element and rows have been seen. A row in 062 * an implicit group, a row group or the end of the table is expected. 063 */ 064 IN_TABLE_AT_POTENTIAL_ROW_GROUP_START, 065 066 /** 067 * A column group is open. It can end or a column can start. 068 */ 069 IN_COLGROUP, 070 071 /** 072 * A column inside a column group is open. It can end. 073 */ 074 IN_COL_IN_COLGROUP, 075 076 /** 077 * A column that is a child of table is open. It can end. 078 */ 079 IN_COL_IN_IMPLICIT_GROUP, 080 081 /** 082 * The open element is an explicit row group. It may end or a row may start. 083 */ 084 IN_ROW_GROUP, 085 086 /** 087 * A row in a an explicit row group is open. It may end or a cell may start. 088 */ 089 IN_ROW_IN_ROW_GROUP, 090 091 /** 092 * A cell inside a row inside an explicit row group is open. It can end. 093 */ 094 IN_CELL_IN_ROW_GROUP, 095 096 /** 097 * A row in an implicit row group is open. It may end or a cell may start. 098 */ 099 IN_ROW_IN_IMPLICIT_ROW_GROUP, 100 101 /** 102 * The table itself is the currently open element, but an implicit row group 103 * been started by previous rows. A row may start, an explicit row group may 104 * start or the table may end. 105 */ 106 IN_IMPLICIT_ROW_GROUP, 107 108 /** 109 * A cell inside an implicit row group is open. It can close. 110 */ 111 IN_CELL_IN_IMPLICIT_ROW_GROUP, 112 113 /** 114 * The table itself is the currently open element. Columns and/or column groups 115 * have been seen but rows or row groups have not been seen yet. A column, a 116 * column group, a row or a row group can start. The table can end. 117 */ 118 IN_TABLE_COLS_SEEN 119 } 120 121 /** 122 * Keeps track of the handler state between SAX events. 123 */ 124 private State state = State.IN_TABLE_AT_START; 125 126 /** 127 * The number of suppressed element starts. 128 */ 129 private int suppressedStarts = 0; 130 131 /** 132 * Indicates whether the width of the table was established by column markup. 133 */ 134 private boolean hardWidth = false; 135 136 /** 137 * The column count established by column markup or by the first row. 138 */ 139 private int columnCount = -1; 140 141 /** 142 * The actual column count as stretched by the widest row. 143 */ 144 private int realColumnCount = 0; 145 146 /** 147 * A colgroup span that hasn't been actuated yet in case the element has 148 * col children. The absolute value counts. The negative sign means that 149 * the value was implied. 150 */ 151 private int pendingColGroupSpan = 0; 152 153 /** 154 * A set of the IDs of header cells. 155 */ 156 private final Set<String> headerIds = new HashSet<String>(); 157 158 /** 159 * A list of cells that refer to headers (in the document order). 160 */ 161 private final List<Cell> cellsReferringToHeaders = new LinkedList<Cell>(); 162 163 /** 164 * The owning checker. 165 */ 166 private final TableChecker owner; 167 168 /** 169 * The current row group (also implicit groups have an explicit object). 170 */ 171 private RowGroup current; 172 173 /** 174 * The head of the column range list. 175 */ 176 private ColumnRange first = null; 177 178 /** 179 * The tail of the column range list. 180 */ 181 private ColumnRange last = null; 182 183 /** 184 * The range under inspection. 185 */ 186 private ColumnRange currentColRange = null; 187 188 /** 189 * The previous range that was inspected. 190 */ 191 private ColumnRange previousColRange = null; 192 193 /** 194 * Constructor. 195 * @param owner reference back to the checker 196 */ 197 public Table(TableChecker owner) { 198 super(); 199 this.owner = owner; 200 } 201 202 private boolean needSuppressStart() { 203 if (suppressedStarts > 0) { 204 suppressedStarts++; 205 return true; 206 } else { 207 return false; 208 } 209 } 210 211 private boolean needSuppressEnd() { 212 if (suppressedStarts > 0) { 213 suppressedStarts--; 214 return true; 215 } else { 216 return false; 217 } 218 } 219 220 void startRowGroup(String type) throws SAXException { 221 if (needSuppressStart()) { 222 return; 223 } 224 switch (state) { 225 case IN_IMPLICIT_ROW_GROUP: 226 current.end(); 227 // fall through 228 case IN_TABLE_AT_START: 229 case IN_TABLE_COLS_SEEN: 230 case IN_TABLE_AT_POTENTIAL_ROW_GROUP_START: 231 current = new RowGroup(this, type); 232 state = State.IN_ROW_GROUP; 233 break; 234 default: 235 suppressedStarts = 1; 236 break; 237 } 238 } 239 240 void endRowGroup() throws SAXException { 241 if (needSuppressEnd()) { 242 return; 243 } 244 switch (state) { 245 case IN_ROW_GROUP: 246 current.end(); 247 current = null; 248 state = State.IN_TABLE_AT_POTENTIAL_ROW_GROUP_START; 249 break; 250 default: 251 throw new IllegalStateException("Bug!"); 252 } 253 } 254 255 void startRow() { 256 if (needSuppressStart()) { 257 return; 258 } 259 switch (state) { 260 case IN_TABLE_AT_START: 261 case IN_TABLE_COLS_SEEN: 262 case IN_TABLE_AT_POTENTIAL_ROW_GROUP_START: 263 current = new RowGroup(this, null); 264 // fall through 265 case IN_IMPLICIT_ROW_GROUP: 266 state = State.IN_ROW_IN_IMPLICIT_ROW_GROUP; 267 break; 268 case IN_ROW_GROUP: 269 state = State.IN_ROW_IN_ROW_GROUP; 270 break; 271 default: 272 suppressedStarts = 1; 273 return; 274 } 275 currentColRange = first; 276 previousColRange = null; 277 current.startRow(); 278 } 279 280 void endRow() throws SAXException { 281 if (needSuppressEnd()) { 282 return; 283 } 284 switch (state) { 285 case IN_ROW_IN_ROW_GROUP: 286 state = State.IN_ROW_GROUP; 287 break; 288 case IN_ROW_IN_IMPLICIT_ROW_GROUP: 289 state = State.IN_IMPLICIT_ROW_GROUP; 290 break; 291 default: 292 throw new IllegalStateException("Bug!"); 293 } 294 current.endRow(); 295 } 296 297 void startCell(boolean header, Attributes attributes) throws SAXException { 298 if (needSuppressStart()) { 299 return; 300 } 301 switch (state) { 302 case IN_ROW_IN_ROW_GROUP: 303 state = State.IN_CELL_IN_ROW_GROUP; 304 break; 305 case IN_ROW_IN_IMPLICIT_ROW_GROUP: 306 state = State.IN_CELL_IN_IMPLICIT_ROW_GROUP; 307 break; 308 default: 309 suppressedStarts = 1; 310 return; 311 } 312 if (header) { 313 int len = attributes.getLength(); 314 for (int i = 0; i < len; i++) { 315 if ("ID".equals(attributes.getType(i))) { 316 String val = attributes.getValue(i); 317 if (!"".equals(val)) { 318 headerIds.add(val); 319 } 320 } 321 } 322 } 323 String[] headers = AttributeUtil.split(attributes.getValue("", 324 "headers")); 325 Cell cell = new Cell( 326 Math.abs(AttributeUtil.parsePositiveInteger(attributes.getValue( 327 "", "colspan"))), 328 Math.abs(AttributeUtil.parseNonNegativeInteger(attributes.getValue( 329 "", "rowspan"))), headers, header, 330 owner.getDocumentLocator(), owner.getErrorHandler()); 331 if (headers.length > 0) { 332 cellsReferringToHeaders.add(cell); 333 } 334 current.cell(cell); 335 } 336 337 void endCell() { 338 if (needSuppressEnd()) { 339 return; 340 } 341 switch (state) { 342 case IN_CELL_IN_ROW_GROUP: 343 state = State.IN_ROW_IN_ROW_GROUP; 344 break; 345 case IN_CELL_IN_IMPLICIT_ROW_GROUP: 346 state = State.IN_ROW_IN_IMPLICIT_ROW_GROUP; 347 break; 348 default: 349 throw new IllegalStateException("Bug!"); 350 } 351 } 352 353 void startColGroup(int span) { 354 if (needSuppressStart()) { 355 return; 356 } 357 switch (state) { 358 case IN_TABLE_AT_START: 359 hardWidth = true; 360 columnCount = 0; 361 // fall through 362 case IN_TABLE_COLS_SEEN: 363 pendingColGroupSpan = span; 364 state = State.IN_COLGROUP; 365 break; 366 default: 367 suppressedStarts = 1; 368 break; 369 } 370 } 371 372 void endColGroup() { 373 if (needSuppressEnd()) { 374 return; 375 } 376 switch (state) { 377 case IN_COLGROUP: 378 if (pendingColGroupSpan != 0) { 379 int right = columnCount + Math.abs(pendingColGroupSpan); 380 Locator locator = new LocatorImpl( 381 owner.getDocumentLocator()); 382 ColumnRange colRange = new ColumnRange("colgroup", locator, 383 columnCount, right); 384 appendColumnRange(colRange); 385 columnCount = right; 386 } 387 realColumnCount = columnCount; 388 state = State.IN_TABLE_COLS_SEEN; 389 break; 390 default: 391 throw new IllegalStateException("Bug!"); 392 } 393 } 394 395 void startCol(int span) throws SAXException { 396 if (needSuppressStart()) { 397 return; 398 } 399 switch (state) { 400 case IN_TABLE_AT_START: 401 hardWidth = true; 402 columnCount = 0; 403 // fall through 404 case IN_TABLE_COLS_SEEN: 405 state = State.IN_COL_IN_IMPLICIT_GROUP; 406 break; 407 case IN_COLGROUP: 408 if (pendingColGroupSpan > 0) { 409 warn("A col element causes a span attribute with value " 410 + pendingColGroupSpan 411 + " to be ignored on the parent colgroup."); 412 } 413 pendingColGroupSpan = 0; 414 state = State.IN_COL_IN_COLGROUP; 415 break; 416 default: 417 suppressedStarts = 1; 418 return; 419 } 420 int right = columnCount + Math.abs(span); 421 Locator locator = new LocatorImpl(owner.getDocumentLocator()); 422 ColumnRange colRange = new ColumnRange("col", locator, 423 columnCount, right); 424 appendColumnRange(colRange); 425 columnCount = right; 426 realColumnCount = columnCount; 427 } 428 429 /** 430 * Appends a column range to the linked list of column ranges. 431 * 432 * @param colRange the range to append 433 */ 434 private void appendColumnRange(ColumnRange colRange) { 435 if (last == null) { 436 first = colRange; 437 last = colRange; 438 } else { 439 last.setNext(colRange); 440 last = colRange; 441 } 442 } 443 444 void warn(String message) throws SAXException { 445 owner.warn(message); 446 } 447 448 void err(String message) throws SAXException { 449 owner.err(message); 450 } 451 452 void endCol() { 453 if (needSuppressEnd()) { 454 return; 455 } 456 switch (state) { 457 case IN_COL_IN_IMPLICIT_GROUP: 458 state = State.IN_TABLE_COLS_SEEN; 459 break; 460 case IN_COL_IN_COLGROUP: 461 state = State.IN_COLGROUP; 462 break; 463 default: 464 throw new IllegalStateException("Bug!"); 465 } 466 } 467 468 void end() throws SAXException { 469 switch (state) { 470 case IN_IMPLICIT_ROW_GROUP: 471 current.end(); 472 current = null; 473 break; 474 case IN_TABLE_AT_START: 475 case IN_TABLE_AT_POTENTIAL_ROW_GROUP_START: 476 case IN_TABLE_COLS_SEEN: 477 break; 478 default: 479 throw new IllegalStateException("Bug!"); 480 } 481 482 // Check referential integrity 483 for (Iterator<Cell> iter = cellsReferringToHeaders.iterator(); iter.hasNext();) { 484 Cell cell = iter.next(); 485 String[] headings = cell.getHeadings(); 486 for (int i = 0; i < headings.length; i++) { 487 String heading = headings[i]; 488 if (!headerIds.contains(heading)) { 489 cell.err("The \u201Cheaders\u201D attribute on the element \u201C" 490 + cell.elementName() 491 + "\u201D refers to the ID \u201C" 492 + heading 493 + "\u201D, but there is no \u201Cth\u201D element with that ID in the same table."); 494 } 495 } 496 } 497 498 // Check that each column has non-extended cells 499 ColumnRange colRange = first; 500 while (colRange != null) { 501 if (colRange.isSingleCol()) { 502 owner.getErrorHandler().error( 503 new SAXParseException("Table column " + colRange 504 + " established by element \u201C" 505 + colRange.getElement() 506 + "\u201D has no cells beginning in it.", 507 colRange.getLocator())); 508 } else { 509 owner.getErrorHandler().error( 510 new SAXParseException("Table columns in range " 511 + colRange + " established by element \u201C" 512 + colRange.getElement() 513 + "\u201D have no cells beginning in them.", 514 colRange.getLocator())); 515 } 516 colRange = colRange.getNext(); 517 } 518 } 519 520 /** 521 * Returns the columnCount. 522 * 523 * @return the columnCount 524 */ 525 int getColumnCount() { 526 return columnCount; 527 } 528 529 /** 530 * Sets the columnCount. 531 * 532 * @param columnCount 533 * the columnCount to set 534 */ 535 void setColumnCount(int columnCount) { 536 this.columnCount = columnCount; 537 } 538 539 /** 540 * Returns the hardWidth. 541 * 542 * @return the hardWidth 543 */ 544 boolean isHardWidth() { 545 return hardWidth; 546 } 547 548 /** 549 * Reports a cell whose positioning has been decided back to the table 550 * so that column bookkeeping can be done. (Called from 551 * <code>RowGroup</code>--not <code>TableChecker</code>.) 552 * 553 * @param cell a cell whose position has been calculated 554 */ 555 void cell(Cell cell) { 556 int left = cell.getLeft(); 557 int right = cell.getRight(); 558 // first see if we've got a cell past the last col 559 if (right > realColumnCount) { 560 // are we past last col entirely? 561 if (left == realColumnCount) { 562 // single col? 563 if (left + 1 != right) { 564 appendColumnRange(new ColumnRange(cell.elementName(), cell, left + 1, right)); 565 } 566 realColumnCount = right; 567 return; 568 } else { 569 // not past entirely 570 appendColumnRange(new ColumnRange(cell.elementName(), cell, realColumnCount, right)); 571 realColumnCount = right; 572 } 573 } 574 while (currentColRange != null) { 575 int hit = currentColRange.hits(left); 576 if (hit == 0) { 577 ColumnRange newRange = currentColRange.removeColumn(left); 578 if (newRange == null) { 579 // zap a list item 580 if (previousColRange != null) { 581 previousColRange.setNext(currentColRange.getNext()); 582 } 583 if (first == currentColRange) { 584 first = currentColRange.getNext(); 585 } 586 if (last == currentColRange) { 587 last = previousColRange; 588 } 589 currentColRange = currentColRange.getNext(); 590 } else { 591 if (last == currentColRange) { 592 last = newRange; 593 } 594 currentColRange = newRange; 595 } 596 return; 597 } else if (hit == -1) { 598 return; 599 } else if (hit == 1) { 600 previousColRange = currentColRange; 601 currentColRange = currentColRange.getNext(); 602 } 603 } 604 } 605 }