package com.limegroup.gnutella.gui.search; import java.util.HashMap; import com.limegroup.gnutella.URN; import com.limegroup.gnutella.gui.tables.BasicDataLineModel; import com.limegroup.gnutella.gui.tables.DataLine; import com.limegroup.gnutella.gui.tables.LimeJTable; import com.limegroup.gnutella.gui.tables.LimeTableColumn; import com.limegroup.gnutella.settings.SearchSettings; import com.limegroup.gnutella.settings.UISettings; /** * Model for search results. * * Ensures that if new lines are added and they are similiar to old lines, * that the new lines are added as extra information to the existing lines, * instead of as brand new lines. */ class ResultPanelModel extends BasicDataLineModel { /** * The model storing metadata information. */ protected final MetadataModel METADATA; /** * The table this is the model of. * This is necessary to fix selection problems caused * by insertion / removal of rows. */ private LimeJTable TABLE; /** * The columns. */ protected final SearchTableColumns COLUMNS = new SearchTableColumns(); /** * Whether or not metadata is being tallied. */ protected boolean _useMetadata = true; /** * HashMap for quick access to indexes based on SHA1 info. */ private final HashMap _indexes = new HashMap(); /** * The TableLineGrouper to use for slow matching. * * Allocated when needed. */ private TableLineGrouper _grouper; /** * The number of sources for this search. */ private int _numSources; /** * Constructs a new ResultPanelModel with a new MetadataModel. */ ResultPanelModel() { this(new MetadataModel()); } /** * Constructs a new ResultPanelModel with the given MetadataModel. */ ResultPanelModel(MetadataModel mm) { super(TableLine.class); METADATA = mm; } /** * Whether or not the line should add to the metadata. */ void setUseMetadata(boolean use) { _useMetadata = use; } /** * Sets the LimeJTable that this ResultPanelModel is for. * * Necessary to fix the selection after moving rows. */ void setTable(LimeJTable table) { TABLE = table; } /** * Gets the columns used by this model. */ SearchTableColumns getColumns() { return COLUMNS; } /** * Creates a new TableLine. */ public DataLine createDataLine() { return new TableLine(COLUMNS); } /** * Gets the column at the specified index. */ public LimeTableColumn getTableColumn(int idx) { return COLUMNS.getColumn(idx); } /** * Overrides default compare to move spam results to the bottom, * or to change the 'count' compare to use different values for * multicast or secure results. */ public int compare(Object a, Object b) { TableLine ta = (TableLine)a; TableLine tb = (TableLine)b; int spamRet = compareSpam(ta, tb); if (spamRet != 0) return spamRet; if (!isSorted() || _activeColumn != SearchTableColumns.COUNT_IDX) return super.compare(ta, tb); else return compareCount(ta, tb, false); } /** * Returns the metadata model storing information about each result * for easy filtering. */ MetadataModel getMetadataModel() { return METADATA; } /** * Overrides the default remove to remove the index from the hashmap. * * @param row the index of the row to remove. */ public void remove(int row) { TableLine tl = (TableLine)get(row); URN sha1 = getSHA1(row); if(sha1 != null) _indexes.remove(sha1); super.remove(row); _numSources -= tl.getLocationCount(); remapIndexes(row); } /** * Override default so new ones get added to the end */ public int add(Object o) { return add(o, getRowCount()); } /** * Override to fix compile error on OSX. */ public int add(DataLine dl) { return super.add(dl); } /** * Override to not iterate through each result. */ public Object refresh() { fireTableRowsUpdated(0, getRowCount()); return null; } /** * Does a slow refresh, forcing the underlying results to have * 'update' called on them. */ public void slowRefresh() { super.refresh(); } /** * Overriden to not get a new dataline if something already exists. */ public DataLine getNewDataLine(Object o) { SearchResult sr = (SearchResult)o; URN sha1 = sr.getRemoteFileDesc().getSHA1Urn(); int idx = -1; if(UISettings.UI_GROUP_RESULTS.getValue()) { if(sha1 == null) idx = slowMatch(sr); else idx = fastMatch(sha1); } if(idx != -1) { TableLine line = (TableLine)get(idx); int added = addNewResult(line, sr); if(added != 0 && isSorted() && TABLE.getTableSettings().REAL_TIME_SORT.getValue() && getSortColumn() == SearchTableColumns.COUNT_IDX) move(line, idx); else if(added != 0) fireTableRowsUpdated(idx, idx); return null; } return super.getNewDataLine(o); } /** * Adds sr to line as a new source. */ protected int addNewResult(TableLine line, SearchResult sr) { int oldCount = line.getLocationCount(); line.addNewResult(sr, METADATA); int newCount = line.getLocationCount(); int added = newCount - oldCount; _numSources += added; return added; } /** * Maintains the indexes HashMap & MetadataModel. */ public int add(DataLine dl, int row) { TableLine tl = (TableLine)dl; _numSources += tl.getLocationCount(); URN sha1 = tl.getSHA1Urn(); if(sha1 != null) _indexes.put(sha1, new Integer(row)); int addedAt = super.add(dl, row); remapIndexes(addedAt + 1); if(_useMetadata) METADATA.addNew(tl); // MUST be after add, else callbacks whack out return addedAt; } /** * Gets the row this DataLine is at. */ public int getRow(DataLine dl) { TableLine tl = (TableLine)dl; URN sha1 = tl.getSHA1Urn(); if(sha1 != null) return fastMatch(sha1); else return super.getRow(dl); } /** * Gets the row this initialize object is at. */ public int getRow(Object o) { SearchResult sr = (SearchResult)o; URN sha1 = sr.getRemoteFileDesc().getSHA1Urn(); if(sha1 != null) return fastMatch(sha1); else return super.getRow(o); } /** * Returns the number of sources found for this search. */ int getTotalSources() { return _numSources; } /** * Overrides the default sort to maintain the indexes HashMap, * according to the current sort column and order. */ protected void doResort() { super.doResort(); _indexes.clear(); // it's easier & quicker to just clear & re-input remapIndexes(0); } /** * Overrides the default clear to erase the indexes HashMap, * Metadata and Grouper. */ public void clear() { if(METADATA != null) METADATA.clear(); if(_grouper != null) _grouper.clear(); simpleClear(); } /** * Does nothing -- lines need no cleanup. */ protected void cleanup() {} /** * Simple clear -- clears the number of sources & cached SHA1 indexes. * Calls super.clear to erase the stored lines. */ protected void simpleClear() { _numSources = 0; _indexes.clear(); super.clear(); } /** * Moves line from oldIdx to somewhere new because its sources updated. */ private void move(TableLine dl, int oldIdx) { int newIdx = oldIdx; if(!isSortAscending()) { // if was at the beginning, update and leave. if(oldIdx == 0) { fireTableRowsUpdated(0, 0); return; } // traverse upwards till we find a line with more. for(int i = 0; newIdx > 0; newIdx--, i++) { TableLine current = (TableLine)get(newIdx-1); if(compareCount(dl, current, true) >= 0) break; } } else { int end = getRowCount() - 1; // if it was at the end, update & leave. if(oldIdx == end) { fireTableRowsUpdated(end, end); return; } // traverse downloads till we find a line with more for(; newIdx < end; newIdx++) { TableLine current = (TableLine)get(newIdx+1); if(compareCount(dl, current, true) >= 0) break; } } // didn't move anywhere? update and leave. if(oldIdx == newIdx) { fireTableRowsUpdated(newIdx, newIdx); return; } // store value for later fix. boolean selected = TABLE.isRowSelected(oldIdx); boolean inView = TABLE.isSelectionVisible(); // we moved from oldIdx to newIdx. super.remove(oldIdx); super.add(dl, newIdx); // *** fix for JTable selection bugs. if(selected) { TABLE.clearSelection(); TABLE.addRowSelectionInterval(newIdx, newIdx); if(inView) TABLE.ensureSelectionVisible(); } else { TABLE.removeRowSelectionInterval(newIdx, newIdx); int selRow = TABLE.getSelectedRow(); if(selRow != -1) { TABLE.addRowSelectionInterval(selRow, selRow); if(inView) TABLE.ensureSelectionVisible(); } } // *** end fix. // remap the indexes that changed. if(oldIdx < newIdx) remapIndexes(oldIdx, newIdx + 1); else remapIndexes(newIdx, oldIdx + 1); } /** * Remaps the indexes, starting at 'start' and going to the end of * the list. This is needed for when rows are added to the middle of * the list to maintain the correct rows per objects. */ private void remapIndexes(int start) { remapIndexes(start, getRowCount()); } /** * Remaps the indexes, starting at 'start' and going to 'end'. * This is useful for when we move a row from end to start or vice versa. */ private void remapIndexes(int start, int end) { for (int i = start; i < end; i++) { URN sha1 = getSHA1(i); if(sha1 != null) _indexes.put(sha1, new Integer(i)); } } /** * Gets the SHA1 URN for a row. */ private URN getSHA1(int idx) { if(idx >= getRowCount()) return null; return ((TableLine)get(idx)).getSHA1Urn(); } /** Compares the spam difference between the two rows. */ private int compareSpam(TableLine a, TableLine b) { if (SearchSettings.moveJunkToBottom()) { if (SpamFilter.isAboveSpamThreshold(a)) { if (!SpamFilter.isAboveSpamThreshold(b)) { return 1; } } else if (SpamFilter.isAboveSpamThreshold(b)) { return -1; } } return 0; } /** * Compares the count between two rows. */ private int compareCount(TableLine a, TableLine b, boolean spamCompare) { if(spamCompare) { int spamRet = compareSpam(a, b); if(spamRet != 0) return spamRet; } int c1 = normalizeLocationCount(a.getLocationCount(), a.getQuality()); int c2 = normalizeLocationCount(b.getLocationCount(), b.getQuality()); return (c1 - c2) * _ascending; } /** Normalizes the location count, depending on the quality. */ private int normalizeLocationCount(int count, int quality) { switch(quality) { case QualityRenderer.SECURE_QUALITY: return Integer.MAX_VALUE; case QualityRenderer.MULTICAST_QUALITY: return Integer.MAX_VALUE-1; default: return count; } } /** * Slow match -- file/size lookups. */ private int slowMatch(SearchResult sr) { if(_grouper == null) _grouper = new TableLineGrouper(); // OK we created a Line out of a response. // Do the grouping. This is expensive! May return null. SearchResult group = _grouper.match(sr); if (group == null) _grouper.add(sr); return super.getRow(group); } /** * Fast match -- lookup in the table. */ private int fastMatch(URN sha1) { Integer idx = (Integer)_indexes.get(sha1); if(idx == null) return -1; else return idx.intValue(); } }