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 + ")"; } } }