package com.limegroup.gnutella.gui.search;
import java.util.Collection;
import java.util.Comparator;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.TreeMap;
import javax.swing.AbstractListModel;
import javax.swing.event.ListDataListener;
import com.limegroup.gnutella.gui.GUIMediator;
import com.limegroup.gnutella.util.Comparators;
import com.limegroup.gnutella.util.DataUtils;
import com.limegroup.gnutella.xml.LimeXMLDocument;
/**
* Maintains information about the metadata in a list of search results.
*
* ListModel views of specific fields may be retrieved in order to
*
*/
final class MetadataModel {
/**
* The resource to use for the extension property.
*/
static final String TYPE = "RESULT_PANEL_TYPE";
/**
* The resource to use for the speed property.
*/
static final String SPEED = "RESULT_PANEL_SPEED";
/**
* The resource to use for the vendor property.
*/
static final String VENDOR = "RESULT_PANEL_VENDOR";
// Important note about the below two mappings MODEL & PROPERTIES:
// ..................................................................
// The values of these MUST be either a ListModelMap or a Collection.
// If the value is a ListModelMap, then recursively the value of that
// map must either be another ListModelMap or a Collection.
// ..................................................................
/**
* A mapping of:
* NamedMediaType (Schema) -> ListModelMap of:
* String (Field Name) -> ListModelMap of:
* String (Value name) -> Collection (TableLine)
*
* This serves to easily look up what table lines match a given value
* within a given field of a given URI.
*
* The Maps also double as ListModels, to return ListModel views of
* sections.
*/
private final ListModelMap MODEL;
/**
* A constant string to use as the field name when a document has no
* fields. This is so we can keep track of the elements in schemas
* that have no schema.
*/
private static final String UNKNOWN = "unknown";
/**
* A second mapping used for keeping track of specific properties, such as
* extension, speed, etc...
*
* The mapping is of:
* String (Property) -> ListModelMap of:
* Object (Value name) -> Collection (TableLine)
*/
private final ListModelMap PROPERTIES;
/**
* Constructs a new MetadataModel.
*/
MetadataModel() {
// Schemas use the natural ordering of the NamedMediaTypes
MODEL = new Model();
// Properties don't need to be case insensitive.
PROPERTIES = new Model(Comparators.stringComparator());
initialize();
}
/**
* Clears this model.
*/
void clear() {
MODEL.clear();
PROPERTIES.clear();
initialize();
}
/**
* Adds a new TableLine, possibly also adding info in the LimeXMLDocument,
* if one exists.
*/
void addNew(TableLine line) {
NamedMediaType mt = line.getNamedMediaType();
// populate the properties map.
addProperties(line);
// no type at all, ignore.
if(mt == null)
return;
Map fieldMap = getMap(MODEL, mt);
LimeXMLDocument doc = line.getXMLDocument();
if(doc != null)
addDocument(fieldMap, doc, line);
else // keep track for the schema.
getCollection(fieldMap, UNKNOWN).add(line);
}
/**
* Removes any references to this table line.
*/
void remove(TableLine line) {
NamedMediaType mt = line.getNamedMediaType();
removeProperties(line);
if(mt == null)
return;
Map fieldMap = getMap(MODEL, mt);
LimeXMLDocument doc = line.getXMLDocument();
if(doc != null)
removeDocument(fieldMap, doc, line);
else
getCollection(fieldMap, UNKNOWN).remove(line);
}
/**
* Adds LimeXMLDocument information to the map.
*
* It is assumed that the schema has already been added.
*/
void addNewDocument(LimeXMLDocument doc, TableLine line) {
NamedMediaType mt = line.getNamedMediaType();
Map fieldMap = getMap(MODEL, mt);
addDocument(fieldMap, doc, line);
}
/**
* Adds the associated the specified schema, field and value to
* given TableLine.
*
* This should only be used when the full document has already been
* added once before.
*/
void addField(String field, String value, TableLine line) {
NamedMediaType mt = line.getNamedMediaType();
Map fieldMap = getMap(MODEL, mt);
Map valueMap = getMap(fieldMap, field);
getCollection(valueMap, value).add(line);
}
/**
* Updates the metadata information for the specified property.
*/
void updateProperty(String property, Object current,
Object old, TableLine line) {
Map map = getMap(PROPERTIES, property);
getCollection(map, old).remove(line);
getCollection(map, current).add(line);
}
/**
* Retrieves the ListModelMap for the specified Selector.
*/
ListModelMap getListModelMap(Selector selector) {
switch(selector.getSelectorType()) {
case Selector.SCHEMA:
return MODEL;
case Selector.FIELD:
NamedMediaType mt = NamedMediaType.getFromDescription(selector.getSchema());
return getMap(getMap(MODEL, mt), selector.getValue());
case Selector.PROPERTY:
return getMap(PROPERTIES, selector.getValue());
}
return null;
}
/**
* Retrieves a list of potential selectors for this model.
*/
List /* of Selector */ getSelectorOptions() {
List list = new LinkedList();
// Always add the 'Schema' option.
list.add(Selector.createSchemaSelector());
// Then add each Field
// First iterate through our schemas
for(Iterator i = MODEL.entrySet().iterator(); i.hasNext();) {
Map.Entry entry = (Map.Entry)i.next();
NamedMediaType nmt = (NamedMediaType)entry.getKey();
String schema = nmt.getMediaType().getMimeType();
// Then add the fields of those schemas.
Iterator fields = ((Map)entry.getValue()).keySet().iterator();
for(; fields.hasNext();) {
String next = (String)fields.next();
if(!UNKNOWN.equals(next))
list.add(Selector.createFieldSelector(schema, next));
}
}
// Then add the properties.
for(Iterator i = PROPERTIES.keySet().iterator(); i.hasNext();) {
list.add(Selector.createPropertySelector((String)i.next()));
}
return list;
}
/**
* Gets a cross section of the two ListModelMaps.
*
* The intend of this method is to filter out any elements from
* the child map that do not correspond with the parent map's selection.
* If the selection is null, it assumes that everything in the parent
* Map is valid.
*
* This works in the following manner... We are provided with
* two ListModelMaps, the parent & the child, as well as the
* selection within the parent. For instance, assume that parent
* is a Map of all audios__audio__artists__, the selection is
* 'Sammy B', and the child is a Map of all audios__audio__albums.
* If child contains entries for 'Piano Hits' and 'Bass Hits',
* but only 'Piano Hits' is by 'Sammy B', then this will return a
* ListModelMap that only contains 'Piano Hits'.
*
* The following steps are used to do this:
* 1) a) Retrieve the element associated with the selection.
* b) If the selection was null or 'All', all parent elements are valid.
* 2) Iterate through the entries in the child map.
* 3) If any of the children's entries exist in the parent's, then
* add that entry to a new map that will be returned.
*/
ListModelMap getCrossSection(ListModelMap parent, Object selection,
ListModelMap child) {
Collection elements;
// STEP 1.
if(selection != null && !isAll(selection)) {
// 1a
Object values = parent.get(selection);
if(values == null)
throw new IllegalArgumentException("invalid selection");
elements = getAllValues(values);
} else {
// 1b
elements = getAllValues(parent);
}
// STEP 2.
// Elements now contains all the Objects that the parent contains.
// We must now iterate through child's elements and retain only those
// elements whose children have an element in elements.
ListModelMap ret = new Model(child.comparator());
for(Iterator i = child.entrySet().iterator(); i.hasNext(); ) {
Map.Entry entry = (Map.Entry)i.next();
// STEP 3.
if(DataUtils.containsAny(elements, getAllValues(entry.getValue())))
ret.put(entry.getKey(), entry.getValue());
}
return ret;
}
/**
* Initializes the maps to the appropriate values.
*/
private void initialize() {
// Ensure that type & speed use natural ordering of the elements.
PROPERTIES.put(TYPE, new Model());
PROPERTIES.put(SPEED, new Model());
}
/**
* Adds the contents of the LimeXMLDocument to the internal maps.
*/
private void addDocument(Map fieldMap, LimeXMLDocument doc, TableLine line) {
boolean added = false;
for(Iterator i = doc.getNameValueSet().iterator(); i.hasNext();) {
added = true;
Map.Entry entry = (Map.Entry)i.next();
String field = (String)entry.getKey();
String value = (String)entry.getValue();
// Retrieve the map of values -> list
Map valueMap = getMap(fieldMap, field);
// Add this value to the ones for this value.
getCollection(valueMap, value).add(line);
}
// if it had no fields, make sure its still counted in the schema.
if(!added)
getCollection(fieldMap, UNKNOWN).add(line);
}
/**
* Removes a references to this line.
*/
private void removeDocument(Map fieldMap, LimeXMLDocument doc, TableLine line) {
boolean removed = false;
for(Iterator i = doc.getNameValueSet().iterator(); i.hasNext();) {
removed = true;
Map.Entry entry = (Map.Entry)i.next();
String field = (String)entry.getKey();
String value = (String)entry.getValue();
// Retrieve the map of values -> list
Map valueMap = getMap(fieldMap, field);
// Add this value to the ones for this value.
getCollection(valueMap, value).remove(line);
}
// if it had no fields, make sure its still counted in the schema.
if(!removed)
getCollection(fieldMap, UNKNOWN).remove(line);
}
/**
* Adds various properties of the TableLine as metadata.
*
* This currently supports:
* extension (RESULT_PANEL_TYPE)
* speed (RESULT_PANEL_SPEED)
* vendor (RESULT_PANEL_VENDOR)
*/
private void addProperties(TableLine line) {
Map extMap = getMap(PROPERTIES, TYPE);
getCollection(extMap, line.getIconAndExtension()).add(line);
Map speedMap = getMap(PROPERTIES, SPEED);
getCollection(speedMap, line.getSpeed()).add(line);
Map vendorMap = getMap(PROPERTIES, VENDOR);
getCollection(vendorMap, line.getVendor()).add(line);
}
/**
* Removes this line from its properties.
*/
private void removeProperties(TableLine line) {
Map extMap = getMap(PROPERTIES, TYPE);
getCollection(extMap, line.getIconAndExtension()).remove(line);
Map speedMap = getMap(PROPERTIES, SPEED);
getCollection(speedMap, line.getSpeed()).remove(line);
Map vendorMap = getMap(PROPERTIES, VENDOR);
getCollection(vendorMap, line.getVendor()).remove(line);
}
/**
* Returns all possible child elements of the Object.
*
* If the object is a Map, it iterates through the values looking
* for either a Map (in which case it recursively calls itself), or a
* Collection (in which case it adds all the contents of the Collection
* to the return value).
*/
private Collection getAllValues(Object parent) {
// already a Collection, return it.
if(parent instanceof Collection)
return (Collection)parent;
if(parent instanceof Map) {
Collection values = new HashSet();
for(Iterator i = ((Map)parent).values().iterator(); i.hasNext();) {
values.addAll(getAllValues(i.next()));
}
return values;
}
// Otherwise we can't handle it.
throw new IllegalArgumentException("parent: " + parent);
}
/**
* Retrieves a map from another map, adding a new one if it didn't exist.
*
* If the the inner map doesn't exist, creates a new one using a case
* insensitive string comparator.
*/
private ListModelMap getMap(Map parent, Object key) {
ListModelMap m = (ListModelMap)parent.get(key);
if(m == null) {
m = new Model(Comparators.caseInsensitiveStringComparator());
parent.put(key, m);
}
return m;
}
/**
* Retrieves a collection from a map, adding one if it didn't exist.
*
* The collection added is a HashSet, although this can be changed.
*/
private Collection getCollection(Map parent, Object key) {
if(key instanceof String) {
// make sure spaces get chopped off.
key = ((String)key).trim();
}
Collection l = (Collection)parent.get(key);
if(l == null) {
l = new HashSet();
parent.put(key, l);
}
return l;
}
/**
* A Model that implements both Map & ListModel.
*/
private static class Model extends TreeMap implements ListModelMap {
/**
* The delegate ListModel for propogating ListModel events.
*/
private final SimpleListModel DELEGATE = new SimpleListModel();
/**
* Constructs the map with a natural ordering of the elements.
*/
Model() {
super();
}
/**
* Constructs the Map with the specified comparator.
*/
Model(Comparator comp) {
super(comp);
}
public Object put(Object a, Object b) {
Object o = super.put(a, b);
DELEGATE.fireContentsChanged(this, 0, size());
return o;
}
public void fireContentsChanged() {
DELEGATE.fireContentsChanged(this, 0, size());
}
/**
* Adds a ListDataListener to the values.
*/
public void addListDataListener(ListDataListener l) {
DELEGATE.addListDataListener(l);
}
/**
* Removes a ListDataListener from the values.
*/
public void removeListDataListener(ListDataListener l) {
DELEGATE.removeListDataListener(l);
}
/**
* Returns the length of the list.
*/
public int getSize() {
return size() + 1; // +1 because of the 'All' element.
}
/**
* Retrieves the element at the specified index.
*/
public Object getElementAt(int idx) {
// The first element to display is always 'All'.
if(idx == 0)
return new All(size());
if(idx > size())
throw new IndexOutOfBoundsException("index: " + idx +
", size: " + getSize());
// TODO: Don't iterate this way.
Iterator i = keySet().iterator();
// Start at 1 because they think we have one more than we do.
for(int j = 1; j < idx; j++)
i.next();
return i.next();
}
/**
* Determines if the ListModel contains the specified object.
*/
public boolean contains(Object o) {
if(isAll(o))
return true;
else
return containsKey(o);
}
/**
* Returns the index of the given value.
*/
public int indexOf(Object o) {
if(isAll(o))
return 0;
else {
Iterator iter = keySet().iterator();
for(int i = 1; iter.hasNext(); i++)
if(compare(o, iter.next()) == 0)
return i;
return -1;
}
}
/**
* Returns the iterator of this map.
*/
public Iterator iterator() {
return keySet().iterator();
}
/**
* Compares two keys using the correct comparison method for this Map.
*/
private int compare(Object k1, Object k2) {
return (comparator()==null ? ((Comparable)k1).compareTo(k2)
: comparator().compare(k1, k2));
}
}
/**
* A simple ListModel, useful for delegating to for action calls.
*/
private static class SimpleListModel extends AbstractListModel {
public int getSize() { throw new IllegalStateException(); }
public Object getElementAt(int idx) { throw new IllegalStateException(); }
public void fireContentsChanged(Object src, int a, int b) {
super.fireContentsChanged(src, a, b);
}
}
/**
* Determines whether or not the specified value is the 'All' selection.
*/
static boolean isAll(Object value) {
return (value instanceof All);
}
/**
* The 'All' selection.
*/
private static class All {
private static final String ALL =
GUIMediator.getStringResource("SEARCH_FILTER_ALL") + " (";
final int number;
private All(int number) {
this.number = number;
}
public String toString() {
return ALL + number + ")";
}
}
}