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 fi.iki.hsivonen.xml.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.xml.sax.Attributes; 032 import org.xml.sax.Locator; 033 import org.xml.sax.SAXException; 034 import org.xml.sax.SAXParseException; 035 import org.xml.sax.helpers.LocatorImpl; 036 037 import fi.iki.hsivonen.xml.AttributeUtil; 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,v 1.7 2006/12/01 12:34:30 hsivonen Exp $ 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 headerIds.add(attributes.getValue(i)); 317 } 318 } 319 } 320 String[] headers = AttributeUtil.split(attributes.getValue("", 321 "headers")); 322 Cell cell = new Cell( 323 Math.abs(AttributeUtil.parsePositiveInteger(attributes.getValue( 324 "", "colspan"))), 325 Math.abs(AttributeUtil.parseNonNegativeInteger(attributes.getValue( 326 "", "rowspan"))), headers, header, 327 owner.getDocumentLocator(), owner.getErrorHandler()); 328 if (headers.length > 0) { 329 cellsReferringToHeaders.add(cell); 330 } 331 current.cell(cell); 332 } 333 334 void endCell() { 335 if (needSuppressEnd()) { 336 return; 337 } 338 switch (state) { 339 case IN_CELL_IN_ROW_GROUP: 340 state = State.IN_ROW_IN_ROW_GROUP; 341 break; 342 case IN_CELL_IN_IMPLICIT_ROW_GROUP: 343 state = State.IN_ROW_IN_IMPLICIT_ROW_GROUP; 344 break; 345 default: 346 throw new IllegalStateException("Bug!"); 347 } 348 } 349 350 void startColGroup(int span) { 351 if (needSuppressStart()) { 352 return; 353 } 354 switch (state) { 355 case IN_TABLE_AT_START: 356 hardWidth = true; 357 columnCount = 0; 358 // fall through 359 case IN_TABLE_COLS_SEEN: 360 pendingColGroupSpan = span; 361 state = State.IN_COLGROUP; 362 break; 363 default: 364 suppressedStarts = 1; 365 break; 366 } 367 } 368 369 void endColGroup() { 370 if (needSuppressEnd()) { 371 return; 372 } 373 switch (state) { 374 case IN_COLGROUP: 375 int right = columnCount + Math.abs(pendingColGroupSpan); 376 Locator locator = new LocatorImpl(owner.getDocumentLocator()); 377 ColumnRange colRange = new ColumnRange("colgroup", locator, 378 columnCount, right); 379 appendColumnRange(colRange); 380 columnCount = right; 381 realColumnCount = columnCount; 382 state = State.IN_TABLE_COLS_SEEN; 383 break; 384 default: 385 throw new IllegalStateException("Bug!"); 386 } 387 } 388 389 void startCol(int span) throws SAXException { 390 if (needSuppressStart()) { 391 return; 392 } 393 switch (state) { 394 case IN_TABLE_AT_START: 395 hardWidth = true; 396 columnCount = 0; 397 // fall through 398 case IN_TABLE_COLS_SEEN: 399 state = State.IN_COL_IN_IMPLICIT_GROUP; 400 break; 401 case IN_COLGROUP: 402 if (pendingColGroupSpan > 0) { 403 warn("A col element causes a span attribute with value " 404 + pendingColGroupSpan 405 + " to be ignored on the parent colgroup."); 406 } 407 pendingColGroupSpan = 0; 408 state = State.IN_COL_IN_COLGROUP; 409 break; 410 default: 411 suppressedStarts = 1; 412 return; 413 } 414 int right = columnCount + Math.abs(span); 415 Locator locator = new LocatorImpl(owner.getDocumentLocator()); 416 ColumnRange colRange = new ColumnRange("col", locator, 417 columnCount, right); 418 appendColumnRange(colRange); 419 columnCount = right; 420 realColumnCount = columnCount; 421 } 422 423 /** 424 * Appends a column range to the linked list of column ranges. 425 * 426 * @param colRange the range to append 427 */ 428 private void appendColumnRange(ColumnRange colRange) { 429 if (last == null) { 430 first = colRange; 431 last = colRange; 432 } else { 433 last.setNext(colRange); 434 last = colRange; 435 } 436 } 437 438 void warn(String message) throws SAXException { 439 owner.warn(message); 440 } 441 442 void err(String message) throws SAXException { 443 owner.err(message); 444 } 445 446 void endCol() { 447 if (needSuppressEnd()) { 448 return; 449 } 450 switch (state) { 451 case IN_COL_IN_IMPLICIT_GROUP: 452 state = State.IN_TABLE_COLS_SEEN; 453 break; 454 case IN_COL_IN_COLGROUP: 455 state = State.IN_COLGROUP; 456 break; 457 default: 458 throw new IllegalStateException("Bug!"); 459 } 460 } 461 462 void end() throws SAXException { 463 switch (state) { 464 case IN_IMPLICIT_ROW_GROUP: 465 current.end(); 466 current = null; 467 break; 468 case IN_TABLE_AT_START: 469 case IN_TABLE_AT_POTENTIAL_ROW_GROUP_START: 470 case IN_TABLE_COLS_SEEN: 471 break; 472 default: 473 throw new IllegalStateException("Bug!"); 474 } 475 476 // Check referential integrity 477 for (Iterator<Cell> iter = cellsReferringToHeaders.iterator(); iter.hasNext();) { 478 Cell cell = iter.next(); 479 String[] headings = cell.getHeadings(); 480 for (int i = 0; i < headings.length; i++) { 481 String heading = headings[i]; 482 if (!headerIds.contains(heading)) { 483 cell.err("The \u201Cheaders\u201D attribute on the element \u201C" 484 + cell.elementName() 485 + "\u201D refers to the ID \u201C" 486 + heading 487 + "\u201D, but there is no \u201Cth\u201D element with that ID in the same table."); 488 } 489 } 490 } 491 492 // Check that each column has non-extended cells 493 ColumnRange colRange = first; 494 while (colRange != null) { 495 if (colRange.isSingleCol()) { 496 owner.getErrorHandler().error( 497 new SAXParseException("Table column " + colRange 498 + " established by element \u201C" 499 + colRange.getElement() 500 + "\u201D has no cells beginning in it.", 501 colRange.getLocator())); 502 } else { 503 owner.getErrorHandler().error( 504 new SAXParseException("Table columns in range " 505 + colRange + " established by element \u201C" 506 + colRange.getElement() 507 + "\u201D have no cells beginning in them.", 508 colRange.getLocator())); 509 } 510 colRange = colRange.getNext(); 511 } 512 } 513 514 /** 515 * Returns the columnCount. 516 * 517 * @return the columnCount 518 */ 519 int getColumnCount() { 520 return columnCount; 521 } 522 523 /** 524 * Sets the columnCount. 525 * 526 * @param columnCount 527 * the columnCount to set 528 */ 529 void setColumnCount(int columnCount) { 530 this.columnCount = columnCount; 531 } 532 533 /** 534 * Returns the hardWidth. 535 * 536 * @return the hardWidth 537 */ 538 boolean isHardWidth() { 539 return hardWidth; 540 } 541 542 /** 543 * Reports a cell whose positioning has been decided back to the table 544 * so that column bookkeeping can be done. (Called from 545 * <code>RowGroup</code>--not <code>TableChecker</code>.) 546 * 547 * @param cell a cell whose position has been calculated 548 */ 549 void cell(Cell cell) { 550 int left = cell.getLeft(); 551 int right = cell.getRight(); 552 // first see if we've got a cell past the last col 553 if (right > realColumnCount) { 554 // are we past last col entirely? 555 if (left == realColumnCount) { 556 // single col? 557 if (left + 1 != right) { 558 appendColumnRange(new ColumnRange(cell.elementName(), cell, left + 1, right)); 559 } 560 realColumnCount = right; 561 return; 562 } else { 563 // not past entirely 564 appendColumnRange(new ColumnRange(cell.elementName(), cell, realColumnCount, right)); 565 realColumnCount = right; 566 } 567 } 568 while (currentColRange != null) { 569 int hit = currentColRange.hits(left); 570 if (hit == 0) { 571 ColumnRange newRange = currentColRange.removeColumn(left); 572 if (newRange == null) { 573 // zap a list item 574 if (previousColRange != null) { 575 previousColRange.setNext(currentColRange.getNext()); 576 } 577 if (first == currentColRange) { 578 first = currentColRange.getNext(); 579 } 580 if (last == currentColRange) { 581 last = previousColRange; 582 } 583 currentColRange = currentColRange.getNext(); 584 } else { 585 if (last == currentColRange) { 586 last = newRange; 587 } 588 currentColRange = newRange; 589 } 590 return; 591 } else if (hit == -1) { 592 return; 593 } else if (hit == 1) { 594 previousColRange = currentColRange; 595 currentColRange = currentColRange.getNext(); 596 } 597 } 598 } 599 }