/* * Copyright (c) 2006 Henri Sivonen * * Permission is hereby granted, free of charge, to any person obtaining a * copy of this software and associated documentation files (the "Software"), * to deal in the Software without restriction, including without limitation * the rights to use, copy, modify, merge, publish, distribute, sublicense, * and/or sell copies of the Software, and to permit persons to whom the * Software is furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER * DEALINGS IN THE SOFTWARE. */ package org.whattf.checker.table; import java.util.HashSet; import java.util.Iterator; import java.util.LinkedList; import java.util.List; import java.util.Set; import org.whattf.checker.AttributeUtil; import org.xml.sax.Attributes; import org.xml.sax.Locator; import org.xml.sax.SAXException; import org.xml.sax.SAXParseException; import org.xml.sax.helpers.LocatorImpl; /** * Represents an XHTML table for table integrity checking. Handles * table-significant parse events and keeps track of columns. * * @version $Id$ * @author hsivonen */ final class Table { /** * An enumeration for keeping track of the parsing state of a table. */ private enum State { /** * The table element start has been seen. No child elements have been seen. * A start of a column, a column group, a row or a row group or the end of * the table is expected. */ IN_TABLE_AT_START, /** * The table element is the open element and rows have been seen. A row in * an implicit group, a row group or the end of the table is expected. */ IN_TABLE_AT_POTENTIAL_ROW_GROUP_START, /** * A column group is open. It can end or a column can start. */ IN_COLGROUP, /** * A column inside a column group is open. It can end. */ IN_COL_IN_COLGROUP, /** * A column that is a child of table is open. It can end. */ IN_COL_IN_IMPLICIT_GROUP, /** * The open element is an explicit row group. It may end or a row may start. */ IN_ROW_GROUP, /** * A row in a an explicit row group is open. It may end or a cell may start. */ IN_ROW_IN_ROW_GROUP, /** * A cell inside a row inside an explicit row group is open. It can end. */ IN_CELL_IN_ROW_GROUP, /** * A row in an implicit row group is open. It may end or a cell may start. */ IN_ROW_IN_IMPLICIT_ROW_GROUP, /** * The table itself is the currently open element, but an implicit row group * been started by previous rows. A row may start, an explicit row group may * start or the table may end. */ IN_IMPLICIT_ROW_GROUP, /** * A cell inside an implicit row group is open. It can close. */ IN_CELL_IN_IMPLICIT_ROW_GROUP, /** * The table itself is the currently open element. Columns and/or column groups * have been seen but rows or row groups have not been seen yet. A column, a * column group, a row or a row group can start. The table can end. */ IN_TABLE_COLS_SEEN } /** * Keeps track of the handler state between SAX events. */ private State state = State.IN_TABLE_AT_START; /** * The number of suppressed element starts. */ private int suppressedStarts = 0; /** * Indicates whether the width of the table was established by column markup. */ private boolean hardWidth = false; /** * The column count established by column markup or by the first row. */ private int columnCount = -1; /** * The actual column count as stretched by the widest row. */ private int realColumnCount = 0; /** * A colgroup span that hasn't been actuated yet in case the element has * col children. The absolute value counts. The negative sign means that * the value was implied. */ private int pendingColGroupSpan = 0; /** * A set of the IDs of header cells. */ private final Set<String> headerIds = new HashSet<String>(); /** * A list of cells that refer to headers (in the document order). */ private final List<Cell> cellsReferringToHeaders = new LinkedList<Cell>(); /** * The owning checker. */ private final TableChecker owner; /** * The current row group (also implicit groups have an explicit object). */ private RowGroup current; /** * The head of the column range list. */ private ColumnRange first = null; /** * The tail of the column range list. */ private ColumnRange last = null; /** * The range under inspection. */ private ColumnRange currentColRange = null; /** * The previous range that was inspected. */ private ColumnRange previousColRange = null; /** * Constructor. * @param owner reference back to the checker */ public Table(TableChecker owner) { super(); this.owner = owner; } private boolean needSuppressStart() { if (suppressedStarts > 0) { suppressedStarts++; return true; } else { return false; } } private boolean needSuppressEnd() { if (suppressedStarts > 0) { suppressedStarts--; return true; } else { return false; } } void startRowGroup(String type) throws SAXException { if (needSuppressStart()) { return; } switch (state) { case IN_IMPLICIT_ROW_GROUP: current.end(); // fall through case IN_TABLE_AT_START: case IN_TABLE_COLS_SEEN: case IN_TABLE_AT_POTENTIAL_ROW_GROUP_START: current = new RowGroup(this, type); state = State.IN_ROW_GROUP; break; default: suppressedStarts = 1; break; } } void endRowGroup() throws SAXException { if (needSuppressEnd()) { return; } switch (state) { case IN_ROW_GROUP: current.end(); current = null; state = State.IN_TABLE_AT_POTENTIAL_ROW_GROUP_START; break; default: throw new IllegalStateException("Bug!"); } } void startRow() { if (needSuppressStart()) { return; } switch (state) { case IN_TABLE_AT_START: case IN_TABLE_COLS_SEEN: case IN_TABLE_AT_POTENTIAL_ROW_GROUP_START: current = new RowGroup(this, null); // fall through case IN_IMPLICIT_ROW_GROUP: state = State.IN_ROW_IN_IMPLICIT_ROW_GROUP; break; case IN_ROW_GROUP: state = State.IN_ROW_IN_ROW_GROUP; break; default: suppressedStarts = 1; return; } currentColRange = first; previousColRange = null; current.startRow(); } void endRow() throws SAXException { if (needSuppressEnd()) { return; } switch (state) { case IN_ROW_IN_ROW_GROUP: state = State.IN_ROW_GROUP; break; case IN_ROW_IN_IMPLICIT_ROW_GROUP: state = State.IN_IMPLICIT_ROW_GROUP; break; default: throw new IllegalStateException("Bug!"); } current.endRow(); } void startCell(boolean header, Attributes attributes) throws SAXException { if (needSuppressStart()) { return; } switch (state) { case IN_ROW_IN_ROW_GROUP: state = State.IN_CELL_IN_ROW_GROUP; break; case IN_ROW_IN_IMPLICIT_ROW_GROUP: state = State.IN_CELL_IN_IMPLICIT_ROW_GROUP; break; default: suppressedStarts = 1; return; } if (header) { int len = attributes.getLength(); for (int i = 0; i < len; i++) { if ("ID".equals(attributes.getType(i))) { String val = attributes.getValue(i); if (!"".equals(val)) { headerIds.add(val); } } } } String[] headers = AttributeUtil.split(attributes.getValue("", "headers")); Cell cell = new Cell( Math.abs(AttributeUtil.parsePositiveInteger(attributes.getValue( "", "colspan"))), Math.abs(AttributeUtil.parseNonNegativeInteger(attributes.getValue( "", "rowspan"))), headers, header, owner.getDocumentLocator(), owner.getErrorHandler()); if (headers.length > 0) { cellsReferringToHeaders.add(cell); } current.cell(cell); } void endCell() { if (needSuppressEnd()) { return; } switch (state) { case IN_CELL_IN_ROW_GROUP: state = State.IN_ROW_IN_ROW_GROUP; break; case IN_CELL_IN_IMPLICIT_ROW_GROUP: state = State.IN_ROW_IN_IMPLICIT_ROW_GROUP; break; default: throw new IllegalStateException("Bug!"); } } void startColGroup(int span) { if (needSuppressStart()) { return; } switch (state) { case IN_TABLE_AT_START: hardWidth = true; columnCount = 0; // fall through case IN_TABLE_COLS_SEEN: pendingColGroupSpan = span; state = State.IN_COLGROUP; break; default: suppressedStarts = 1; break; } } void endColGroup() { if (needSuppressEnd()) { return; } switch (state) { case IN_COLGROUP: if (pendingColGroupSpan != 0) { int right = columnCount + Math.abs(pendingColGroupSpan); Locator locator = new LocatorImpl( owner.getDocumentLocator()); ColumnRange colRange = new ColumnRange("colgroup", locator, columnCount, right); appendColumnRange(colRange); columnCount = right; } realColumnCount = columnCount; state = State.IN_TABLE_COLS_SEEN; break; default: throw new IllegalStateException("Bug!"); } } void startCol(int span) throws SAXException { if (needSuppressStart()) { return; } switch (state) { case IN_TABLE_AT_START: hardWidth = true; columnCount = 0; // fall through case IN_TABLE_COLS_SEEN: state = State.IN_COL_IN_IMPLICIT_GROUP; break; case IN_COLGROUP: if (pendingColGroupSpan > 0) { warn("A col element causes a span attribute with value " + pendingColGroupSpan + " to be ignored on the parent colgroup."); } pendingColGroupSpan = 0; state = State.IN_COL_IN_COLGROUP; break; default: suppressedStarts = 1; return; } int right = columnCount + Math.abs(span); Locator locator = new LocatorImpl(owner.getDocumentLocator()); ColumnRange colRange = new ColumnRange("col", locator, columnCount, right); appendColumnRange(colRange); columnCount = right; realColumnCount = columnCount; } /** * Appends a column range to the linked list of column ranges. * * @param colRange the range to append */ private void appendColumnRange(ColumnRange colRange) { if (last == null) { first = colRange; last = colRange; } else { last.setNext(colRange); last = colRange; } } void warn(String message) throws SAXException { owner.warn(message); } void err(String message) throws SAXException { owner.err(message); } void endCol() { if (needSuppressEnd()) { return; } switch (state) { case IN_COL_IN_IMPLICIT_GROUP: state = State.IN_TABLE_COLS_SEEN; break; case IN_COL_IN_COLGROUP: state = State.IN_COLGROUP; break; default: throw new IllegalStateException("Bug!"); } } void end() throws SAXException { switch (state) { case IN_IMPLICIT_ROW_GROUP: current.end(); current = null; break; case IN_TABLE_AT_START: case IN_TABLE_AT_POTENTIAL_ROW_GROUP_START: case IN_TABLE_COLS_SEEN: break; default: throw new IllegalStateException("Bug!"); } // Check referential integrity for (Iterator<Cell> iter = cellsReferringToHeaders.iterator(); iter.hasNext();) { Cell cell = iter.next(); String[] headings = cell.getHeadings(); for (int i = 0; i < headings.length; i++) { String heading = headings[i]; if (!headerIds.contains(heading)) { cell.err("The \u201Cheaders\u201D attribute on the element \u201C" + cell.elementName() + "\u201D refers to the ID \u201C" + heading + "\u201D, but there is no \u201Cth\u201D element with that ID in the same table."); } } } // Check that each column has non-extended cells ColumnRange colRange = first; while (colRange != null) { if (colRange.isSingleCol()) { owner.getErrorHandler().error( new SAXParseException("Table column " + colRange + " established by element \u201C" + colRange.getElement() + "\u201D has no cells beginning in it.", colRange.getLocator())); } else { owner.getErrorHandler().error( new SAXParseException("Table columns in range " + colRange + " established by element \u201C" + colRange.getElement() + "\u201D have no cells beginning in them.", colRange.getLocator())); } colRange = colRange.getNext(); } } /** * Returns the columnCount. * * @return the columnCount */ int getColumnCount() { return columnCount; } /** * Sets the columnCount. * * @param columnCount * the columnCount to set */ void setColumnCount(int columnCount) { this.columnCount = columnCount; } /** * Returns the hardWidth. * * @return the hardWidth */ boolean isHardWidth() { return hardWidth; } /** * Reports a cell whose positioning has been decided back to the table * so that column bookkeeping can be done. (Called from * <code>RowGroup</code>--not <code>TableChecker</code>.) * * @param cell a cell whose position has been calculated */ void cell(Cell cell) { int left = cell.getLeft(); int right = cell.getRight(); // first see if we've got a cell past the last col if (right > realColumnCount) { // are we past last col entirely? if (left == realColumnCount) { // single col? if (left + 1 != right) { appendColumnRange(new ColumnRange(cell.elementName(), cell, left + 1, right)); } realColumnCount = right; return; } else { // not past entirely appendColumnRange(new ColumnRange(cell.elementName(), cell, realColumnCount, right)); realColumnCount = right; } } while (currentColRange != null) { int hit = currentColRange.hits(left); if (hit == 0) { ColumnRange newRange = currentColRange.removeColumn(left); if (newRange == null) { // zap a list item if (previousColRange != null) { previousColRange.setNext(currentColRange.getNext()); } if (first == currentColRange) { first = currentColRange.getNext(); } if (last == currentColRange) { last = previousColRange; } currentColRange = currentColRange.getNext(); } else { if (last == currentColRange) { last = newRange; } currentColRange = newRange; } return; } else if (hit == -1) { return; } else if (hit == 1) { previousColRange = currentColRange; currentColRange = currentColRange.getNext(); } } } }