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

hazendaz / httpunit / 656

06 Dec 2025 09:11PM UTC coverage: 80.452% (+0.02%) from 80.435%
656

push

github

hazendaz
[maven-release-plugin] prepare for next development iteration

3213 of 4105 branches covered (78.27%)

Branch coverage included in aggregate %.

8245 of 10137 relevant lines covered (81.34%)

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
 * MIT License
3
 *
4
 * Copyright 2011-2025 Russell Gold
5
 *
6
 * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated
7
 * documentation files (the "Software"), to deal in the Software without restriction, including without limitation
8
 * the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and
9
 * to permit persons to whom the Software is furnished to do so, subject to the following conditions:
10
 *
11
 * The above copyright notice and this permission notice shall be included in all copies or substantial portions
12
 * of the Software.
13
 *
14
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO
15
 * THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF
17
 * CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
18
 * DEALINGS IN THE SOFTWARE.
19
 */
20
package com.meterware.httpunit;
21

22
import com.meterware.httpunit.scripting.ScriptableDelegate;
23

24
import java.net.URL;
25
import java.util.ArrayList;
26
import java.util.Enumeration;
27
import java.util.Hashtable;
28

29
import org.w3c.dom.Element;
30
import org.w3c.dom.Node;
31
import org.w3c.dom.html.HTMLTableCellElement;
32
import org.w3c.dom.html.HTMLTableRowElement;
33

34
/**
35
 * This class represents a table in an HTML page.
36
 **/
37
public class WebTable extends HTMLElementBase {
38

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

42
    /** Predicate to match a prefix of a table's first non-blank cell. **/
43
    public static final HTMLElementPredicate MATCH_FIRST_NONBLANK_CELL_PREFIX;
44

45
    /** Predicate to match a table's summary attribute. **/
46
    public static final HTMLElementPredicate MATCH_SUMMARY;
47

48
    /** Predicate to match a table's ID. **/
49
    public static final HTMLElementPredicate MATCH_ID;
50

51
    /**
52
     * Returns the number of rows in the table.
53
     *
54
     * @return the row count
55
     */
56
    public int getRowCount() {
57
        return getCells().length;
1✔
58
    }
59

60
    /**
61
     * Gets the cells.
62
     *
63
     * @return the cells
64
     */
65
    private TableCell[][] getCells() {
66
        if (_cells == null) {
1✔
67
            readTable();
1✔
68
        }
69
        return _cells;
1✔
70

71
    }
72

73
    /**
74
     * Returns the number of columns in the table.
75
     *
76
     * @return the column count
77
     */
78
    public int getColumnCount() {
79
        if (getCells().length == 0) {
1!
80
            return 0;
×
81
        }
82
        return getCells()[0].length;
1✔
83
    }
84

85
    /**
86
     * Returns the contents of the specified table cell as text. The row and column numbers are zero-based.
87
     *
88
     * @param row
89
     *            the row
90
     * @param column
91
     *            the column
92
     *
93
     * @return the cell as text
94
     *
95
     * @throws IndexOutOfBoundsException
96
     *             if the specified cell numbers are not valid
97
     */
98
    public String getCellAsText(int row, int column) {
99
        TableCell cell = getTableCell(row, column);
1✔
100
        return cell == null ? "" : cell.getText();
1✔
101
    }
102

103
    /**
104
     * Returns the contents of the specified table cell as text. The row and column numbers are zero-based.
105
     *
106
     * @param row
107
     *            the row
108
     * @param column
109
     *            the column
110
     *
111
     * @return the table cell
112
     *
113
     * @throws IndexOutOfBoundsException
114
     *             if the specified cell numbers are not valid
115
     */
116
    public TableCell getTableCell(int row, int column) {
117
        return getCells()[row][column];
1✔
118
    }
119

120
    /**
121
     * Returns the contents of the specified table cell with a given ID.
122
     *
123
     * @param id
124
     *            the id
125
     *
126
     * @return TableCell with given ID or null if ID is not found.
127
     */
128
    public TableCell getTableCellWithID(String id) {
129
        for (int i = 0; i < getRowCount(); i++) {
1✔
130
            for (int j = 0; j < getColumnCount(); j++) {
1✔
131
                final TableCell tableCell = getCells()[i][j];
1✔
132
                if (tableCell != null && tableCell.getID().equals(id)) {
1✔
133
                    return tableCell;
1✔
134
                }
135
            }
136
        }
137
        return null;
1✔
138
    }
139

140
    /**
141
     * Removes all rows and all columns from this table which have no visible text in them. patch [ 1117822 ] Patch for
142
     * purgeEmptyCells() problem by Glen Stampoultzis
143
     **/
144
    public void purgeEmptyCells() {
145
        int numRowsWithText = 0;
1✔
146
        int numColumnsWithText = 0;
1✔
147
        boolean[] rowHasText = new boolean[getRowCount()];
1✔
148
        boolean[] columnHasText = new boolean[getColumnCount()];
1✔
149
        Hashtable spanningCells = new Hashtable<>();
1✔
150

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

186
        // look for requirements to keep spanning cells: special processing is needed if either:
187
        // none of its rows already have text, or none of its columns already have text.
188
        for (Enumeration e = spanningCells.keys(); e.hasMoreElements();) {
1✔
189
            TableCell cell = (TableCell) e.nextElement();
1✔
190
            int[] coords = (int[]) spanningCells.get(cell);
1✔
191
            boolean neededInRow = true;
1✔
192
            boolean neededInCol = true;
1✔
193
            for (int i = coords[0]; neededInRow && i < rowHasText.length && i < coords[0] + cell.getRowSpan(); i++) {
1!
194
                neededInRow = !rowHasText[i];
1!
195
            }
196
            for (int j = coords[1]; neededInCol && j < columnHasText.length && j < coords[1] + cell.getColSpan(); j++) {
1!
197
                neededInCol = !columnHasText[j];
1!
198
            }
199
            if (neededInRow) {
1!
200
                rowHasText[coords[0]] = true;
×
201
                numRowsWithText++;
×
202
            }
203
            if (neededInCol) {
1!
204
                columnHasText[coords[1]] = true;
×
205
                numColumnsWithText++;
×
206
            }
207
        }
1✔
208

209
        TableCell[][] remainingCells = new TableCell[numRowsWithText][numColumnsWithText];
1✔
210

211
        int targetRow = 0;
1✔
212
        for (int i = 0; i < rowHasText.length; i++) {
1✔
213
            if (!rowHasText[i]) {
1✔
214
                continue;
1✔
215
            }
216
            int targetColumn = 0;
1✔
217
            for (int j = 0; j < columnHasText.length; j++) {
1✔
218
                if (!columnHasText[j]) {
1✔
219
                    continue;
1✔
220
                }
221
                remainingCells[targetRow][targetColumn] = _cells[i][j];
1✔
222
                targetColumn++;
1✔
223
            }
224
            targetRow++;
1✔
225
        }
226

227
        _cells = remainingCells;
1✔
228

229
    }
1✔
230

231
    /**
232
     * Returns a rendering of this table with all cells converted to text.
233
     *
234
     * @return the string[][]
235
     */
236
    public String[][] asText() {
237
        String[][] result = new String[getRowCount()][getColumnCount()];
1✔
238

239
        for (int i = 0; i < result.length; i++) {
1✔
240
            for (int j = 0; j < result[0].length; j++) {
1✔
241
                result[i][j] = getCellAsText(i, j);
1✔
242
            }
243
        }
244
        return result;
1✔
245
    }
246

247
    /**
248
     * Returns the summary attribute associated with this table.
249
     *
250
     * @return the summary
251
     */
252
    public String getSummary() {
253
        return NodeUtils.getNodeAttribute(_dom, "summary");
1✔
254
    }
255

256
    @Override
257
    public String toString() {
258
        String eol = System.lineSeparator();
1✔
259
        StringBuilder sb = new StringBuilder(HttpUnitUtils.DEFAULT_TEXT_BUFFER_SIZE).append("WebTable:").append(eol);
1✔
260
        for (int i = 0; i < getCells().length; i++) {
1✔
261
            sb.append("[").append(i).append("]: ");
1✔
262
            for (int j = 0; j < getCells()[i].length; j++) {
1✔
263
                sb.append("  [").append(j).append("]=");
1✔
264
                if (getCells()[i][j] == null) {
1!
265
                    sb.append("null");
×
266
                } else {
267
                    sb.append(getCells()[i][j].getText());
1✔
268
                }
269
            }
270
            sb.append(eol);
1✔
271
        }
272
        return sb.toString();
1✔
273
    }
274

275
    @Override
276
    public ScriptableDelegate newScriptable() {
277
        return new HTMLElementScriptable(this);
1✔
278
    }
279

280
    @Override
281
    public ScriptableDelegate getParentDelegate() {
282
        return _response.getDocumentScriptable();
1✔
283
    }
284

285
    // ----------------------------------- private members -----------------------------------
286

287
    /** The dom. */
288
    private Element _dom;
289

290
    /** The url. */
291
    private URL _url;
292

293
    /** The frame name. */
294
    private FrameSelector _frameName;
295

296
    /** The base target. */
297
    private String _baseTarget;
298

299
    /** The character set. */
300
    private String _characterSet;
301

302
    /** The response. */
303
    private WebResponse _response;
304

305
    /** The cells. */
306
    private TableCell[][] _cells;
307

308
    /**
309
     * Instantiates a new web table.
310
     *
311
     * @param response
312
     *            the response
313
     * @param frame
314
     *            the frame
315
     * @param domTreeRoot
316
     *            the dom tree root
317
     * @param sourceURL
318
     *            the source URL
319
     * @param baseTarget
320
     *            the base target
321
     * @param characterSet
322
     *            the character set
323
     */
324
    WebTable(WebResponse response, FrameSelector frame, Node domTreeRoot, URL sourceURL, String baseTarget,
325
            String characterSet) {
326
        super(domTreeRoot);
1✔
327
        _response = response;
1✔
328
        _frameName = frame;
1✔
329
        _dom = (Element) domTreeRoot;
1✔
330
        _url = sourceURL;
1✔
331
        _baseTarget = baseTarget;
1✔
332
        _characterSet = characterSet;
1✔
333
    }
1✔
334

335
    /**
336
     * Read table.
337
     */
338
    private void readTable() {
339
        TableRow[] rows = getRows();
1✔
340
        int[] columnsRequired = new int[rows.length];
1✔
341

342
        for (int i = 0; i < rows.length; i++) {
1✔
343
            TableCell[] cells = rows[i].getCells();
1✔
344
            for (TableCell cell : cells) {
1✔
345
                int spannedRows = Math.min(columnsRequired.length - i, cell.getRowSpan());
1✔
346
                for (int k = 0; k < spannedRows; k++) {
1✔
347
                    columnsRequired[i + k] += cell.getColSpan();
1✔
348
                }
349
            }
350
        }
351
        int numColumns = 0;
1✔
352
        for (int element : columnsRequired) {
1✔
353
            numColumns = Math.max(numColumns, element);
1✔
354
        }
355

356
        _cells = new TableCell[columnsRequired.length][numColumns];
1✔
357

358
        for (int i = 0; i < rows.length; i++) {
1✔
359
            TableCell[] cells = rows[i].getCells();
1✔
360
            for (int j = 0; j < cells.length; j++) {
1✔
361
                int spannedRows = Math.min(columnsRequired.length - i, cells[j].getRowSpan());
1✔
362
                for (int k = 0; k < spannedRows; k++) {
1✔
363
                    for (int l = 0; l < cells[j].getColSpan(); l++) {
1✔
364
                        placeCell(i + k, j + l, cells[j]);
1✔
365
                    }
366
                }
367
            }
368
        }
369
    }
1✔
370

371
    /**
372
     * Place cell.
373
     *
374
     * @param row
375
     *            the row
376
     * @param column
377
     *            the column
378
     * @param cell
379
     *            the cell
380
     */
381
    private void placeCell(int row, int column, TableCell cell) {
382
        while (_cells[row][column] != null) {
1✔
383
            column++;
1✔
384
        }
385
        _cells[row][column] = cell;
1✔
386
    }
1✔
387

388
    /** The rows. */
389
    private ArrayList _rows = new ArrayList<>();
1✔
390

391
    /**
392
     * Adds the row.
393
     *
394
     * @param tableRow
395
     *            the table row
396
     */
397
    void addRow(TableRow tableRow) {
398
        _cells = null;
1✔
399
        _rows.add(tableRow);
1✔
400
    }
1✔
401

402
    /**
403
     * New table row.
404
     *
405
     * @param element
406
     *            the element
407
     *
408
     * @return the table row
409
     */
410
    TableRow newTableRow(HTMLTableRowElement element) {
411
        return new TableRow(this, element);
1✔
412
    }
413

414
    /**
415
     * Returns an array of rows for this table.
416
     *
417
     * @return the rows
418
     */
419
    public TableRow[] getRows() {
420
        return (TableRow[]) _rows.toArray(new TableRow[_rows.size()]);
1✔
421
    }
422

423
    /**
424
     * New table cell.
425
     *
426
     * @param element
427
     *            the element
428
     *
429
     * @return the table cell
430
     */
431
    TableCell newTableCell(HTMLTableCellElement element) {
432
        return new TableCell(_response, _frameName, element, _url, _baseTarget, _characterSet);
1✔
433
    }
434

435
    static {
436
        MATCH_FIRST_NONBLANK_CELL = (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.matches(table.getCellAsText(row, col).trim(), (String) criteria)) {
1✔
441
                        return true;
1✔
442
                    }
443
                }
444
            }
445
            return false;
1✔
446
        };
447

448
        MATCH_FIRST_NONBLANK_CELL_PREFIX = (htmlElement, criteria) -> {
1✔
449
            WebTable table = (WebTable) htmlElement;
1✔
450
            for (int row = 0; row < table.getRowCount(); row++) {
1✔
451
                for (int col = 0; col < table.getColumnCount(); col++) {
1✔
452
                    if (HttpUnitUtils.hasPrefix(table.getCellAsText(row, col).trim(), (String) criteria)) {
1✔
453
                        return true;
1✔
454
                    }
455
                }
456
            }
457
            return false;
1✔
458
        };
459

460
        MATCH_ID = (htmlElement, criteria) -> HttpUnitUtils.matches(((WebTable) htmlElement).getID(),
1✔
461
                (String) criteria);
462

463
        MATCH_SUMMARY = (htmlElement, criteria) -> HttpUnitUtils.matches(((WebTable) htmlElement).getSummary(),
1✔
464
                (String) criteria);
465

466
    }
1✔
467

468
}
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