View Javadoc
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       * &lt;td&gt;cell1&lt;/td&gt; &lt;td colspan='2'&gt;cell2&lt;/td&gt; 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 }