• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

hazendaz / httpunit / 755

14 Feb 2026 07:14PM UTC coverage: 80.526%. Remained the same
755

push

github

hazendaz
[ci] Fix badge

3213 of 4105 branches covered (78.27%)

Branch coverage included in aggregate %.

8245 of 10124 relevant lines covered (81.44%)

0.81 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

93.2
/src/main/java/com/meterware/httpunit/WebTable.java
1
/*
2
 * SPDX-License-Identifier: MIT
3
 * See LICENSE file for details.
4
 *
5
 * Copyright 2000-2026 Russell Gold
6
 * Copyright 2021-2000 hazendaz
7
 */
8
package com.meterware.httpunit;
9

10
import com.meterware.httpunit.scripting.ScriptableDelegate;
11

12
import java.net.URL;
13
import java.util.ArrayList;
14
import java.util.Enumeration;
15
import java.util.Hashtable;
16

17
import org.w3c.dom.Element;
18
import org.w3c.dom.Node;
19
import org.w3c.dom.html.HTMLTableCellElement;
20
import org.w3c.dom.html.HTMLTableRowElement;
21

22
/**
23
 * This class represents a table in an HTML page.
24
 **/
25
public class WebTable extends HTMLElementBase {
26

27
    /** Predicate to match the complete text of a table's first non-blank cell. **/
28
    public static final HTMLElementPredicate MATCH_FIRST_NONBLANK_CELL;
29

30
    /** Predicate to match a prefix of a table's first non-blank cell. **/
31
    public static final HTMLElementPredicate MATCH_FIRST_NONBLANK_CELL_PREFIX;
32

33
    /** Predicate to match a table's summary attribute. **/
34
    public static final HTMLElementPredicate MATCH_SUMMARY;
35

36
    /** Predicate to match a table's ID. **/
37
    public static final HTMLElementPredicate MATCH_ID;
38

39
    /**
40
     * Returns the number of rows in the table.
41
     *
42
     * @return the row count
43
     */
44
    public int getRowCount() {
45
        return getCells().length;
1✔
46
    }
47

48
    /**
49
     * Gets the cells.
50
     *
51
     * @return the cells
52
     */
53
    private TableCell[][] getCells() {
54
        if (_cells == null) {
1✔
55
            readTable();
1✔
56
        }
57
        return _cells;
1✔
58

59
    }
60

61
    /**
62
     * Returns the number of columns in the table.
63
     *
64
     * @return the column count
65
     */
66
    public int getColumnCount() {
67
        if (getCells().length == 0) {
1!
68
            return 0;
×
69
        }
70
        return getCells()[0].length;
1✔
71
    }
72

73
    /**
74
     * Returns the contents of the specified table cell as text. The row and column numbers are zero-based.
75
     *
76
     * @param row
77
     *            the row
78
     * @param column
79
     *            the column
80
     *
81
     * @return the cell as text
82
     *
83
     * @throws IndexOutOfBoundsException
84
     *             if the specified cell numbers are not valid
85
     */
86
    public String getCellAsText(int row, int column) {
87
        TableCell cell = getTableCell(row, column);
1✔
88
        return cell == null ? "" : cell.getText();
1✔
89
    }
90

91
    /**
92
     * Returns the contents of the specified table cell as text. The row and column numbers are zero-based.
93
     *
94
     * @param row
95
     *            the row
96
     * @param column
97
     *            the column
98
     *
99
     * @return the table cell
100
     *
101
     * @throws IndexOutOfBoundsException
102
     *             if the specified cell numbers are not valid
103
     */
104
    public TableCell getTableCell(int row, int column) {
105
        return getCells()[row][column];
1✔
106
    }
107

108
    /**
109
     * Returns the contents of the specified table cell with a given ID.
110
     *
111
     * @param id
112
     *            the id
113
     *
114
     * @return TableCell with given ID or null if ID is not found.
115
     */
116
    public TableCell getTableCellWithID(String id) {
117
        for (int i = 0; i < getRowCount(); i++) {
1✔
118
            for (int j = 0; j < getColumnCount(); j++) {
1✔
119
                final TableCell tableCell = getCells()[i][j];
1✔
120
                if (tableCell != null && tableCell.getID().equals(id)) {
1✔
121
                    return tableCell;
1✔
122
                }
123
            }
124
        }
125
        return null;
1✔
126
    }
127

128
    /**
129
     * Removes all rows and all columns from this table which have no visible text in them. patch [ 1117822 ] Patch for
130
     * purgeEmptyCells() problem by Glen Stampoultzis
131
     **/
132
    public void purgeEmptyCells() {
133
        int numRowsWithText = 0;
1✔
134
        int numColumnsWithText = 0;
1✔
135
        boolean[] rowHasText = new boolean[getRowCount()];
1✔
136
        boolean[] columnHasText = new boolean[getColumnCount()];
1✔
137
        Hashtable spanningCells = new Hashtable<>();
1✔
138

139
        // look for rows and columns with any text in a non-spanning cell
140
        for (int row = 0; row < rowHasText.length; row++) {
1✔
141
            for (int col = 0; col < columnHasText.length; col++) {
1✔
142
                if (getCellAsText(row, col).trim().isEmpty()) {
1✔
143
                    continue;
1✔
144
                }
145
                if (getTableCell(row, col).getColSpan() == 1 && getTableCell(row, col).getRowSpan() == 1) {
1✔
146
                    if (!rowHasText[row]) {
1✔
147
                        numRowsWithText++;
1✔
148
                    }
149
                    if (!columnHasText[col]) {
1✔
150
                        numColumnsWithText++;
1✔
151
                    }
152
                    rowHasText[row] = columnHasText[col] = true;
1✔
153
                } else // gstamp: altered the original code to deal with two issues:
154
                       // Firstly only the coordinates of the first spanning cell
155
                       // are added to the Map. The old code was using the last
156
                       // set of coordinates.
157
                       // Secondly I mark the starting coordinates as containing
158
                       // text which keeps the next section of code from removing
159
                       // the starting row/column.
160
                if (!spanningCells.containsKey(getTableCell(row, col))
1✔
161
                        && spanningCells.get(getTableCell(row, col)) == null) {
1!
162
                    if (!rowHasText[row]) {
1✔
163
                        numRowsWithText++;
1✔
164
                    }
165
                    if (!columnHasText[col]) {
1✔
166
                        numColumnsWithText++;
1✔
167
                    }
168
                    rowHasText[row] = columnHasText[col] = true;
1✔
169
                    spanningCells.put(getTableCell(row, col), new int[] { row, col });
1✔
170
                }
171
            }
172
        }
173

174
        // look for requirements to keep spanning cells: special processing is needed if either:
175
        // none of its rows already have text, or none of its columns already have text.
176
        for (Enumeration e = spanningCells.keys(); e.hasMoreElements();) {
1✔
177
            TableCell cell = (TableCell) e.nextElement();
1✔
178
            int[] coords = (int[]) spanningCells.get(cell);
1✔
179
            boolean neededInRow = true;
1✔
180
            boolean neededInCol = true;
1✔
181
            for (int i = coords[0]; neededInRow && i < rowHasText.length && i < coords[0] + cell.getRowSpan(); i++) {
1!
182
                neededInRow = !rowHasText[i];
1!
183
            }
184
            for (int j = coords[1]; neededInCol && j < columnHasText.length && j < coords[1] + cell.getColSpan(); j++) {
1!
185
                neededInCol = !columnHasText[j];
1!
186
            }
187
            if (neededInRow) {
1!
188
                rowHasText[coords[0]] = true;
×
189
                numRowsWithText++;
×
190
            }
191
            if (neededInCol) {
1!
192
                columnHasText[coords[1]] = true;
×
193
                numColumnsWithText++;
×
194
            }
195
        }
1✔
196

197
        TableCell[][] remainingCells = new TableCell[numRowsWithText][numColumnsWithText];
1✔
198

199
        int targetRow = 0;
1✔
200
        for (int i = 0; i < rowHasText.length; i++) {
1✔
201
            if (!rowHasText[i]) {
1✔
202
                continue;
1✔
203
            }
204
            int targetColumn = 0;
1✔
205
            for (int j = 0; j < columnHasText.length; j++) {
1✔
206
                if (!columnHasText[j]) {
1✔
207
                    continue;
1✔
208
                }
209
                remainingCells[targetRow][targetColumn] = _cells[i][j];
1✔
210
                targetColumn++;
1✔
211
            }
212
            targetRow++;
1✔
213
        }
214

215
        _cells = remainingCells;
1✔
216

217
    }
1✔
218

219
    /**
220
     * Returns a rendering of this table with all cells converted to text.
221
     *
222
     * @return the string[][]
223
     */
224
    public String[][] asText() {
225
        String[][] result = new String[getRowCount()][getColumnCount()];
1✔
226

227
        for (int i = 0; i < result.length; i++) {
1✔
228
            for (int j = 0; j < result[0].length; j++) {
1✔
229
                result[i][j] = getCellAsText(i, j);
1✔
230
            }
231
        }
232
        return result;
1✔
233
    }
234

235
    /**
236
     * Returns the summary attribute associated with this table.
237
     *
238
     * @return the summary
239
     */
240
    public String getSummary() {
241
        return NodeUtils.getNodeAttribute(_dom, "summary");
1✔
242
    }
243

244
    @Override
245
    public String toString() {
246
        String eol = System.lineSeparator();
1✔
247
        StringBuilder sb = new StringBuilder(HttpUnitUtils.DEFAULT_TEXT_BUFFER_SIZE).append("WebTable:").append(eol);
1✔
248
        for (int i = 0; i < getCells().length; i++) {
1✔
249
            sb.append("[").append(i).append("]: ");
1✔
250
            for (int j = 0; j < getCells()[i].length; j++) {
1✔
251
                sb.append("  [").append(j).append("]=");
1✔
252
                if (getCells()[i][j] == null) {
1!
253
                    sb.append("null");
×
254
                } else {
255
                    sb.append(getCells()[i][j].getText());
1✔
256
                }
257
            }
258
            sb.append(eol);
1✔
259
        }
260
        return sb.toString();
1✔
261
    }
262

263
    @Override
264
    public ScriptableDelegate newScriptable() {
265
        return new HTMLElementScriptable(this);
1✔
266
    }
267

268
    @Override
269
    public ScriptableDelegate getParentDelegate() {
270
        return _response.getDocumentScriptable();
1✔
271
    }
272

273
    // ----------------------------------- private members -----------------------------------
274

275
    /** The dom. */
276
    private Element _dom;
277

278
    /** The url. */
279
    private URL _url;
280

281
    /** The frame name. */
282
    private FrameSelector _frameName;
283

284
    /** The base target. */
285
    private String _baseTarget;
286

287
    /** The character set. */
288
    private String _characterSet;
289

290
    /** The response. */
291
    private WebResponse _response;
292

293
    /** The cells. */
294
    private TableCell[][] _cells;
295

296
    /**
297
     * Instantiates a new web table.
298
     *
299
     * @param response
300
     *            the response
301
     * @param frame
302
     *            the frame
303
     * @param domTreeRoot
304
     *            the dom tree root
305
     * @param sourceURL
306
     *            the source URL
307
     * @param baseTarget
308
     *            the base target
309
     * @param characterSet
310
     *            the character set
311
     */
312
    WebTable(WebResponse response, FrameSelector frame, Node domTreeRoot, URL sourceURL, String baseTarget,
313
            String characterSet) {
314
        super(domTreeRoot);
1✔
315
        _response = response;
1✔
316
        _frameName = frame;
1✔
317
        _dom = (Element) domTreeRoot;
1✔
318
        _url = sourceURL;
1✔
319
        _baseTarget = baseTarget;
1✔
320
        _characterSet = characterSet;
1✔
321
    }
1✔
322

323
    /**
324
     * Read table.
325
     */
326
    private void readTable() {
327
        TableRow[] rows = getRows();
1✔
328
        int[] columnsRequired = new int[rows.length];
1✔
329

330
        for (int i = 0; i < rows.length; i++) {
1✔
331
            TableCell[] cells = rows[i].getCells();
1✔
332
            for (TableCell cell : cells) {
1✔
333
                int spannedRows = Math.min(columnsRequired.length - i, cell.getRowSpan());
1✔
334
                for (int k = 0; k < spannedRows; k++) {
1✔
335
                    columnsRequired[i + k] += cell.getColSpan();
1✔
336
                }
337
            }
338
        }
339
        int numColumns = 0;
1✔
340
        for (int element : columnsRequired) {
1✔
341
            numColumns = Math.max(numColumns, element);
1✔
342
        }
343

344
        _cells = new TableCell[columnsRequired.length][numColumns];
1✔
345

346
        for (int i = 0; i < rows.length; i++) {
1✔
347
            TableCell[] cells = rows[i].getCells();
1✔
348
            for (int j = 0; j < cells.length; j++) {
1✔
349
                int spannedRows = Math.min(columnsRequired.length - i, cells[j].getRowSpan());
1✔
350
                for (int k = 0; k < spannedRows; k++) {
1✔
351
                    for (int l = 0; l < cells[j].getColSpan(); l++) {
1✔
352
                        placeCell(i + k, j + l, cells[j]);
1✔
353
                    }
354
                }
355
            }
356
        }
357
    }
1✔
358

359
    /**
360
     * Place cell.
361
     *
362
     * @param row
363
     *            the row
364
     * @param column
365
     *            the column
366
     * @param cell
367
     *            the cell
368
     */
369
    private void placeCell(int row, int column, TableCell cell) {
370
        while (_cells[row][column] != null) {
1✔
371
            column++;
1✔
372
        }
373
        _cells[row][column] = cell;
1✔
374
    }
1✔
375

376
    /** The rows. */
377
    private ArrayList _rows = new ArrayList<>();
1✔
378

379
    /**
380
     * Adds the row.
381
     *
382
     * @param tableRow
383
     *            the table row
384
     */
385
    void addRow(TableRow tableRow) {
386
        _cells = null;
1✔
387
        _rows.add(tableRow);
1✔
388
    }
1✔
389

390
    /**
391
     * New table row.
392
     *
393
     * @param element
394
     *            the element
395
     *
396
     * @return the table row
397
     */
398
    TableRow newTableRow(HTMLTableRowElement element) {
399
        return new TableRow(this, element);
1✔
400
    }
401

402
    /**
403
     * Returns an array of rows for this table.
404
     *
405
     * @return the rows
406
     */
407
    public TableRow[] getRows() {
408
        return (TableRow[]) _rows.toArray(new TableRow[_rows.size()]);
1✔
409
    }
410

411
    /**
412
     * New table cell.
413
     *
414
     * @param element
415
     *            the element
416
     *
417
     * @return the table cell
418
     */
419
    TableCell newTableCell(HTMLTableCellElement element) {
420
        return new TableCell(_response, _frameName, element, _url, _baseTarget, _characterSet);
1✔
421
    }
422

423
    static {
424
        MATCH_FIRST_NONBLANK_CELL = (htmlElement, criteria) -> {
1✔
425
            WebTable table = (WebTable) htmlElement;
1✔
426
            for (int row = 0; row < table.getRowCount(); row++) {
1✔
427
                for (int col = 0; col < table.getColumnCount(); col++) {
1✔
428
                    if (HttpUnitUtils.matches(table.getCellAsText(row, col).trim(), (String) criteria)) {
1✔
429
                        return true;
1✔
430
                    }
431
                }
432
            }
433
            return false;
1✔
434
        };
435

436
        MATCH_FIRST_NONBLANK_CELL_PREFIX = (htmlElement, criteria) -> {
1✔
437
            WebTable table = (WebTable) htmlElement;
1✔
438
            for (int row = 0; row < table.getRowCount(); row++) {
1✔
439
                for (int col = 0; col < table.getColumnCount(); col++) {
1✔
440
                    if (HttpUnitUtils.hasPrefix(table.getCellAsText(row, col).trim(), (String) criteria)) {
1✔
441
                        return true;
1✔
442
                    }
443
                }
444
            }
445
            return false;
1✔
446
        };
447

448
        MATCH_ID = (htmlElement, criteria) -> HttpUnitUtils.matches(((WebTable) htmlElement).getID(),
1✔
449
                (String) criteria);
450

451
        MATCH_SUMMARY = (htmlElement, criteria) -> HttpUnitUtils.matches(((WebTable) htmlElement).getSummary(),
1✔
452
                (String) criteria);
453

454
    }
1✔
455

456
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc