1 /*
2 * Copyright (c) 2002-2026 Gargoyle Software Inc.
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 * https://www.apache.org/licenses/LICENSE-2.0
8 *
9 * Unless required by applicable law or agreed to in writing, software
10 * distributed under the License is distributed on an "AS IS" BASIS,
11 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 * See the License for the specific language governing permissions and
13 * limitations under the License.
14 */
15 package org.htmlunit.html;
16
17 import java.util.ArrayList;
18 import java.util.Collections;
19 import java.util.HashSet;
20 import java.util.Iterator;
21 import java.util.List;
22 import java.util.Map;
23 import java.util.NoSuchElementException;
24
25 import org.htmlunit.ElementNotFoundException;
26 import org.htmlunit.SgmlPage;
27 import org.htmlunit.util.geometry.Point2D;
28
29 /**
30 * Wrapper for the HTML element "table".
31 *
32 * @author Mike Bowler
33 * @author David K. Taylor
34 * @author Christian Sell
35 * @author Ahmed Ashour
36 * @author Ronald Brill
37 * @author Frank Danek
38 */
39 public class HtmlTable extends HtmlElement {
40
41 /** The HTML tag represented by this element. */
42 public static final String TAG_NAME = "table";
43
44 /**
45 * Creates an instance.
46 *
47 * @param qualifiedName the qualified name of the element type to instantiate
48 * @param page the page that contains this element
49 * @param attributes the initial attributes
50 */
51 HtmlTable(final String qualifiedName, final SgmlPage page,
52 final Map<String, DomAttr> attributes) {
53 super(qualifiedName, page, attributes);
54 }
55
56 /**
57 * Returns the first cell that matches the specified row and column, searching left to right, top to bottom.
58 * <p>This method returns different values than getRow(rowIndex).getCell(cellIndex) because this takes cellspan
59 * and rowspan into account.<br>
60 * This means, a cell with colspan='2' consumes two columns; a cell with rowspan='3' consumes three rows. The
61 * index is based on the 'background' model of the table; if you have a row like<br>
62 * <td>cell1</td> <td colspan='2'>cell2</td> then this row is treated as a row with
63 * three cells.
64 * </p>
65 * <p>
66 * <code>
67 * getCellAt(rowIndex, 0).asText() returns "cell1";<br>
68 * getCellAt(rowIndex, 1).asText() returns "cell2";<br>
69 * getCellAt(rowIndex, 2).asText() returns "cell2"; and<br>
70 * getCellAt(rowIndex, 3).asText() returns null;
71 * </code>
72 * </p>
73 *
74 * @param rowIndex the row index
75 * @param columnIndex the column index
76 * @return the HtmlTableCell at that location or null if there are no cells at that location
77 */
78 public final HtmlTableCell getCellAt(final int rowIndex, final int columnIndex) {
79 final RowIterator rowIterator = getRowIterator();
80 final HashSet<Point2D> occupied = new HashSet<>();
81 int row = 0;
82 for (final HtmlTableRow htmlTableRow : rowIterator) {
83 final HtmlTableRow.CellIterator cellIterator = htmlTableRow.getCellIterator();
84 int col = 0;
85 for (final HtmlTableCell cell : cellIterator) {
86 while (occupied.contains(new Point2D(row, col))) {
87 col++;
88 }
89 final int nextRow = row + cell.getRowSpan();
90 if (row <= rowIndex && nextRow > rowIndex) {
91 final int nextCol = col + cell.getColumnSpan();
92 if (col <= columnIndex && nextCol > columnIndex) {
93 return cell;
94 }
95 }
96 final int rowSpan = cell.getRowSpan();
97 final int columnSpan = cell.getColumnSpan();
98 if (rowSpan > 1 || columnSpan > 1) {
99 for (int i = 0; i < rowSpan; i++) {
100 for (int j = 0; j < columnSpan; j++) {
101 occupied.add(new Point2D(row + i, col + j));
102 }
103 }
104 }
105 col++;
106 }
107 row++;
108 }
109 return null;
110 }
111
112 /**
113 * Returns an iterator over all rows in this table.
114 *
115 * @return an iterator over all {@link HtmlTableRow} objects
116 */
117 private RowIterator getRowIterator() {
118 return new RowIterator();
119 }
120
121 /**
122 * Returns an immutable list of all rows in this table.
123 *
124 * @return an immutable list containing all {@link HtmlTableRow} objects
125 * @see #getRowIterator()
126 */
127 public List<HtmlTableRow> getRows() {
128 final List<HtmlTableRow> result = new ArrayList<>();
129 for (final HtmlTableRow row : getRowIterator()) {
130 result.add(row);
131 }
132 return Collections.unmodifiableList(result);
133 }
134
135 /**
136 * Returns the row at the specified index.
137 *
138 * @param index the 0-based index of the row
139 * @return the {@link HtmlTableRow} at the given index
140 * @throws IndexOutOfBoundsException if there is no row at the given index
141 * @see #getRowIterator()
142 */
143 public HtmlTableRow getRow(final int index) throws IndexOutOfBoundsException {
144 int count = 0;
145 for (final HtmlTableRow row : getRowIterator()) {
146 if (count == index) {
147 return row;
148 }
149 count++;
150 }
151 throw new IndexOutOfBoundsException("No row found for index " + index + ".");
152 }
153
154 /**
155 * Computes the number of rows in this table. Note that the count is computed dynamically
156 * by iterating over all rows.
157 *
158 * @return the number of rows in this table
159 */
160 public final int getRowCount() {
161 int count = 0;
162 for (final RowIterator iterator = getRowIterator(); iterator.hasNext(); iterator.next()) {
163 count++;
164 }
165 return count;
166 }
167
168 /**
169 * Returns the row with the specified identifier.
170 *
171 * @param id the row identifier
172 * @return the row with the specified identifier
173 * @throws ElementNotFoundException if the row cannot be found
174 */
175 public final HtmlTableRow getRowById(final String id) throws ElementNotFoundException {
176 for (final HtmlTableRow row : getRowIterator()) {
177 if (row.getId().equals(id)) {
178 return row;
179 }
180 }
181 throw new ElementNotFoundException("tr", DomElement.ID_ATTRIBUTE, id);
182 }
183
184 /**
185 * Returns the table caption text or an empty string if a caption wasn't specified.
186 *
187 * @return the caption text
188 */
189 public String getCaptionText() {
190 for (final DomElement element : getChildElements()) {
191 if (element instanceof HtmlCaption) {
192 return element.asNormalizedText();
193 }
194 }
195 return null;
196 }
197
198 /**
199 * Returns the table header or null if a header wasn't specified.
200 *
201 * @return the table header
202 */
203 public HtmlTableHeader getHeader() {
204 for (final DomElement element : getChildElements()) {
205 if (element instanceof HtmlTableHeader header) {
206 return header;
207 }
208 }
209 return null;
210 }
211
212 /**
213 * Returns the table footer or null if a footer wasn't specified.
214 *
215 * @return the table footer
216 */
217 public HtmlTableFooter getFooter() {
218 for (final DomElement element : getChildElements()) {
219 if (element instanceof HtmlTableFooter footer) {
220 return footer;
221 }
222 }
223 return null;
224 }
225
226 /**
227 * Returns a list of tables bodies defined in this table. If no bodies were defined
228 * then an empty list will be returned.
229 *
230 * @return a list of {@link HtmlTableBody} objects
231 */
232 public List<HtmlTableBody> getBodies() {
233 final List<HtmlTableBody> bodies = new ArrayList<>();
234 for (final DomElement element : getChildElements()) {
235 if (element instanceof HtmlTableBody body) {
236 bodies.add(body);
237 }
238 }
239 return bodies;
240 }
241
242 /**
243 * Returns the value of the attribute {@code summary}. Refer to the
244 * <a href="http://www.w3.org/TR/html401/">HTML 4.01</a>
245 * documentation for details on the use of this attribute.
246 *
247 * @return the value of the attribute {@code summary}
248 * or an empty string if that attribute isn't defined.
249 */
250 public final String getSummaryAttribute() {
251 return getAttributeDirect("summary");
252 }
253
254 /**
255 * Returns the value of the attribute {@code width}. Refer to the
256 * <a href="http://www.w3.org/TR/html401/">HTML 4.01</a>
257 * documentation for details on the use of this attribute.
258 *
259 * @return the value of the attribute {@code width}
260 * or an empty string if that attribute isn't defined.
261 */
262 public final String getWidthAttribute() {
263 return getAttributeDirect("width");
264 }
265
266 /**
267 * Returns the value of the attribute {@code border}. Refer to the
268 * <a href="http://www.w3.org/TR/html401/">HTML 4.01</a>
269 * documentation for details on the use of this attribute.
270 *
271 * @return the value of the attribute {@code border}
272 * or an empty string if that attribute isn't defined.
273 */
274 public final String getBorderAttribute() {
275 return getAttributeDirect("border");
276 }
277
278 /**
279 * Returns the value of the attribute {@code frame}. Refer to the
280 * <a href="http://www.w3.org/TR/html401/">HTML 4.01</a>
281 * documentation for details on the use of this attribute.
282 *
283 * @return the value of the attribute {@code frame}
284 * or an empty string if that attribute isn't defined.
285 */
286 public final String getFrameAttribute() {
287 return getAttributeDirect("frame");
288 }
289
290 /**
291 * Returns the value of the attribute {@code rules}. Refer to the
292 * <a href="http://www.w3.org/TR/html401/">HTML 4.01</a>
293 * documentation for details on the use of this attribute.
294 *
295 * @return the value of the attribute {@code rules}
296 * or an empty string if that attribute isn't defined.
297 */
298 public final String getRulesAttribute() {
299 return getAttributeDirect("rules");
300 }
301
302 /**
303 * Returns the value of the attribute {@code cellspacing}. Refer to the
304 * <a href="http://www.w3.org/TR/html401/">HTML 4.01</a>
305 * documentation for details on the use of this attribute.
306 *
307 * @return the value of the attribute {@code cellspacing}
308 * or an empty string if that attribute isn't defined.
309 */
310 public final String getCellSpacingAttribute() {
311 return getAttributeDirect("cellspacing");
312 }
313
314 /**
315 * Returns the value of the attribute {@code cellpadding}. Refer to the
316 * <a href="http://www.w3.org/TR/html401/">HTML 4.01</a>
317 * documentation for details on the use of this attribute.
318 *
319 * @return the value of the attribute {@code cellpadding}
320 * or an empty string if that attribute isn't defined.
321 */
322 public final String getCellPaddingAttribute() {
323 return getAttributeDirect("cellpadding");
324 }
325
326 /**
327 * Returns the value of the attribute {@code align}. Refer to the
328 * <a href="http://www.w3.org/TR/html401/">HTML 4.01</a>
329 * documentation for details on the use of this attribute.
330 *
331 * @return the value of the attribute {@code align}
332 * or an empty string if that attribute isn't defined.
333 */
334 public final String getAlignAttribute() {
335 return getAttributeDirect("align");
336 }
337
338 /**
339 * Returns the value of the attribute {@code bgcolor}. Refer to the
340 * <a href="http://www.w3.org/TR/html401/">HTML 4.01</a>
341 * documentation for details on the use of this attribute.
342 *
343 * @return the value of the attribute {@code bgcolor}
344 * or an empty string if that attribute isn't defined.
345 */
346 public final String getBgcolorAttribute() {
347 return getAttributeDirect("bgcolor");
348 }
349
350 /**
351 * An iterator that moves over all rows in this table. The iterator will also
352 * enter into nested row group elements (header, footer and body).
353 */
354 private class RowIterator implements Iterator<HtmlTableRow>, Iterable<HtmlTableRow> {
355 private HtmlTableRow nextRow_;
356 private TableRowGroup currentGroup_;
357
358 /** Creates a new instance. */
359 RowIterator() {
360 setNextRow(getFirstChild());
361 }
362
363 /**
364 * {@inheritDoc}
365 */
366 @Override
367 public boolean hasNext() {
368 return nextRow_ != null;
369 }
370
371 /**
372 * {@inheritDoc}
373 */
374 @Override
375 public HtmlTableRow next() throws NoSuchElementException {
376 return nextRow();
377 }
378
379 /**
380 * {@inheritDoc}
381 */
382 @Override
383 public void remove() {
384 if (nextRow_ == null) {
385 throw new IllegalStateException();
386 }
387 final DomNode sibling = nextRow_.getPreviousSibling();
388 if (sibling != null) {
389 sibling.remove();
390 }
391 }
392
393 /**
394 * Returns the next row.
395 *
396 * @return the next row from this iterator
397 * @throws NoSuchElementException if no more rows are available
398 */
399 public HtmlTableRow nextRow() throws NoSuchElementException {
400 if (nextRow_ != null) {
401 final HtmlTableRow result = nextRow_;
402 setNextRow(nextRow_.getNextSibling());
403 return result;
404 }
405 throw new NoSuchElementException();
406 }
407
408 /**
409 * Sets the internal position to the next row, starting at the given node.
410 * @param node the node to mark as the next row; if this is not a row, the
411 * next reachable row will be marked.
412 */
413 private void setNextRow(final DomNode node) {
414 nextRow_ = null;
415 for (DomNode next = node; next != null; next = next.getNextSibling()) {
416 if (next instanceof HtmlTableRow row) {
417 nextRow_ = row;
418 return;
419 }
420 else if (currentGroup_ == null && next instanceof TableRowGroup group) {
421 currentGroup_ = group;
422 setNextRow(next.getFirstChild());
423 return;
424 }
425 }
426 if (currentGroup_ != null) {
427 final DomNode group = currentGroup_;
428 currentGroup_ = null;
429 setNextRow(group.getNextSibling());
430 }
431 }
432
433 /**
434 * {@inheritDoc}
435 */
436 @Override
437 public Iterator<HtmlTableRow> iterator() {
438 return this;
439 }
440 }
441
442 /**
443 * {@inheritDoc}
444 *
445 * @return {@code true} so that generated XML uses explicit opening and closing
446 * {@code <table>} tags
447 */
448 @Override
449 protected boolean isEmptyXmlTagExpanded() {
450 return true;
451 }
452
453 /**
454 * {@inheritDoc}
455 */
456 @Override
457 public DisplayStyle getDefaultStyleDisplay() {
458 return DisplayStyle.TABLE;
459 }
460 }