/* * Zed Attack Proxy (ZAP) and its related class files. * * ZAP is an HTTP/HTTPS proxy for assessing web application security. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.zaproxy.zap.utils; import java.awt.EventQueue; import java.util.ArrayList; import java.util.Collections; import java.util.List; import java.util.SortedSet; import java.util.TreeSet; import javax.swing.event.TableModelListener; import javax.swing.table.AbstractTableModel; import org.apache.log4j.Logger; /** * A paginated {@code TableModel}. The model will have at most, by default, {@value #DEFAULT_MAX_PAGE_SIZE} rows loaded in * memory at any given time. The advertised row count will be of all the entries (as if they were all loaded in memory). * <p> * If a {@code JTable}, using this model, is wrapped in a {@code JScrollPane} the vertical scroll bar will be shown as if all * the entries were loaded. * </p> * <p> * Rows (page segments) will be loaded in a separate thread, on demand. * </p> * <p> * Implementation based on PagingTableModel located in {@literal http://www.coderanch.com/t/345383/GUI/java/JTable-Paging}, with * permission from the author, Brian Cole (bassclar@world.oberlin.edu, http://bitguru.com).<br> * Contains the following changes: * <ul> * <li>Removed simulation code;</li> * <li>Added type parameter;</li> * <li>Added abstract methods.</li> * </ul> * * @param <T> the type of elements in this table model * @see #loadPage(int, int) * @see #setMaxPageSize(int) * @see javax.swing.table.TableModel * @see javax.swing.JTable * @see javax.swing.JScrollPane */ public abstract class PagingTableModel<T> extends AbstractTableModel { private static final long serialVersionUID = -6353414328926478100L; private static final Logger logger = Logger.getLogger(PagingTableModel.class); /** * Default segment loader thread name. */ public static final String DEFAULT_SEGMENT_LOADER_THREAD_NAME = "ZAP-PagingTableModel-SegmentLoaderThread"; /** * Default maximum page size. */ public static final int DEFAULT_MAX_PAGE_SIZE = 50; /** * The maximum size of the page. */ private int maxPageSize; private int dataOffset = 0; private List<T> data = Collections.emptyList(); private SortedSet<Segment> pending = new TreeSet<>(); private final String segmentLoaderThreadName; /** * Constructs a {@code PagingTableModel} with default default segment loader thread name ( * {@value #DEFAULT_SEGMENT_LOADER_THREAD_NAME}) and default maximum page size ({@value #DEFAULT_MAX_PAGE_SIZE}). */ public PagingTableModel() { this(DEFAULT_SEGMENT_LOADER_THREAD_NAME, DEFAULT_MAX_PAGE_SIZE); } /** * Constructs a {@code PagingTableModel} with the given segment loader thread name and default maximum page size * ({@value #DEFAULT_MAX_PAGE_SIZE}). * * @param segmentLoaderThreadName the name for segment loader thread * @throws IllegalArgumentException if {@code maxPageSize} is negative or zero. */ public PagingTableModel(String segmentLoaderThreadName) { this(segmentLoaderThreadName, DEFAULT_MAX_PAGE_SIZE); } /** * Constructs a {@code PagingTableModel} with the given maximum page size and default segment loader thread name ( * {@value #DEFAULT_SEGMENT_LOADER_THREAD_NAME}). * * @param maxPageSize the maximum page size * @throws IllegalArgumentException if {@code maxPageSize} is negative or zero. */ public PagingTableModel(final int maxPageSize) { this(DEFAULT_SEGMENT_LOADER_THREAD_NAME, maxPageSize); } /** * Constructs a {@code PagingTableModel} with the given segment loader thread name and given maximum page size. * * @param segmentLoaderThreadName the name for segment loader thread * @param maxPageSize the maximum page size * @throws IllegalArgumentException if {@code maxPageSize} is negative or zero. */ public PagingTableModel(String segmentLoaderThreadName, int maxPageSize) { this.segmentLoaderThreadName = segmentLoaderThreadName; setMaxPageSizeWithoutPageChanges(maxPageSize); } /** * Returns the maximum page size. * * @return the maximum page size. */ public int getMaxPageSize() { return maxPageSize; } /** * Sets the maximum size of the page. * <p> * If the given maximum size is greater than the current maximum size a new page will be loaded, otherwise the current page * will be shrunk to meet the given maximum size. In both cases the {@code TableModelListener} will be notified of the * change. * </p> * <p> * The call to this method has no effect if the given maximum size is equal to the current maximum size. * </p> * * @param maxPageSize the new maximum page size * @throws IllegalArgumentException if {@code maxPageSize} is negative or zero. * @see #setMaxPageSizeWithoutPageChanges(int) * @see TableModelListener */ public void setMaxPageSize(final int maxPageSize) { if (maxPageSize <= 0) { throw new IllegalArgumentException("Parameter maxPageSize must be greater than zero."); } if (this.maxPageSize == maxPageSize) { return; } int oldMaxPageSize = this.maxPageSize; setMaxPageSizeWithoutPageChanges(maxPageSize); int rowCount = getRowCount(); if (rowCount > 0) { if (maxPageSize > oldMaxPageSize) { schedule(dataOffset); } else if (data.size() > maxPageSize) { final List<T> shrunkData = data.subList(0, maxPageSize); EventQueue.invokeLater(new Runnable() { @Override public void run() { setData(dataOffset, new ArrayList<>(shrunkData)); } }); } } } /** * Sets the maximum size of the page. * <p> * As opposed to method {@code #setMaxPageSize(int)} no changes will be made to the current page. * </p> * * @param maxPageSize the new maximum page size * @throws IllegalArgumentException if {@code maxPageSize} is negative or zero. * @see #setMaxPageSize(int) */ public void setMaxPageSizeWithoutPageChanges(final int maxPageSize) { if (maxPageSize <= 0) { throw new IllegalArgumentException("Parameter maxPageSize must be greater than zero."); } this.maxPageSize = maxPageSize; } @Override public void fireTableDataChanged() { // clear cached data clear(); super.fireTableDataChanged(); } /** * Returns the number of all items. * * @return number of items */ @Override public abstract int getRowCount(); /** * Called by {@link PagingTableModel#getValueAt(int, int)} when requested * row is already loaded. * * @param rowObject the row object * @param columnIndex the column index * @return value from requested column */ protected abstract Object getRealValueAt(T rowObject, int columnIndex); /** * Gets the placeholder value that should be shown for the given column, until the actual values are ready to be shown. * * @param columnIndex the column index * @return Value is used to display while loading entry */ protected abstract Object getPlaceholderValueAt(int columnIndex); /** * Called when a new page is required. * <p> * The returned {@code List} should support fast (preferably constant time) random access. * </p> * * @param offset the start offset of the page * @param length the length of the page * @return an excerpt of whole list * @see List#get(int) */ protected abstract List<T> loadPage(int offset, int length); @Override public final Object getValueAt(int rowIndex, int columnIndex) { T rowObject = getRowObject(rowIndex); if (rowObject == null) { // is not loaded yet schedule(rowIndex); // return default value meanwhile return getPlaceholderValueAt(columnIndex); } return getRealValueAt(rowObject, columnIndex); } /** * Gets the object at the given row. * * @param rowIndex the index of the row * @return {@code null} if object is not in the current page */ protected T getRowObject(int rowIndex) { int pageIndex = rowIndex - dataOffset; if (pageIndex >= 0 && pageIndex < data.size()) { return data.get(pageIndex); } return null; } /** * Schedule the loading of the neighborhood around offset (if not already * scheduled). * * @param offset the offset row */ private void schedule(int offset) { if (isPending(offset)) { return; } int startOffset = Math.max(0, offset - maxPageSize / 2); int length = offset + maxPageSize / 2 - startOffset; load(startOffset, length); } private boolean isPending(int offset) { int pendingCount = pending.size(); if (pendingCount == 0) { return false; } if (pendingCount == 1) { // special case (for speed) Segment seg = pending.first(); return seg.contains(offset); } Segment low = new Segment(offset - maxPageSize, 0); Segment high = new Segment(offset + 1, 0); // search pending segments that may contain offset for (Segment seg : pending.subSet(low, high)) { if (seg.contains(offset)) { return true; } } return false; } private void load(final int startOffset, final int length) { Segment seg = new Segment(startOffset, length); pending.add(seg); SegmentLoaderThread segmentLoader = new SegmentLoaderThread(seg, segmentLoaderThreadName); segmentLoader.start(); } /** * Sets the given {@code page} as the currently loaded data and notifies the table model listeners of the rows updated. * <p> * <strong>Note:</strong> This method must be call on the EDT, failing to do so might result in GUI state inconsistencies. * </p> * * @param offset the start offset of the given {@code page} * @param page the new data * @see EventQueue#invokeLater(Runnable) * @see TableModelListener */ private void setData(int offset, List<T> page) { int lastRow = offset + page.size() - 1; dataOffset = offset; data = page; fireTableRowsUpdated(offset, lastRow); } protected void clear() { data.clear(); data = Collections.emptyList(); pending.clear(); } /** * This class is used to keep track of which rows have been scheduled for * loading, so that rows don't get scheduled twice concurrently. The idea is * to store Segments in a sorted data structure for fast searching. * <p> * The compareTo() method sorts first by base position, then by length. */ static final class Segment implements Comparable<Segment> { private final int base; private final int length; public Segment(int base, int length) { this.base = base; this.length = length; } public int getBase() { return base; } public int getLength() { return length; } public boolean contains(int pos) { return (base <= pos && pos < base + length); } @Override public boolean equals(Object o) { if (o != null && o instanceof Segment) { Segment s = (Segment) o; boolean hasSameBase = (base == s.base); boolean hasSameLength = (length == s.length); return hasSameBase && hasSameLength; } return false; } @Override public int hashCode() { return (41 * (41 + base) + length); } @Override public int compareTo(Segment other) { // return negative/zero/positive as this object is // less-than/equal-to/greater-than other int d = base - other.base; if (d != 0) { return d; } return length - other.length; } } private class SegmentLoaderThread extends Thread { private final Segment segment; public SegmentLoaderThread(Segment segment, String name) { super(name); this.segment = segment; } @Override public void run() { final List<T> page; try { page = loadPage(segment.getBase(), segment.getLength()); } catch (Exception e) { logger.warn("error retrieving page at " + segment.getBase() + ": aborting", e); pending.remove(segment); return; } EventQueue.invokeLater(new Runnable() { @Override public void run() { setData(segment.getBase(), page); pending.remove(segment); } }); } } }