package pathways.util { import flash.events.Event; import flash.events.EventDispatcher; import flash.utils.getQualifiedClassName; import mx.collections.IList; import mx.core.IMXMLObject; import mx.events.CollectionEvent; import mx.events.CollectionEventKind; import mx.events.PropertyChangeEvent; import mx.logging.ILogger; import mx.logging.Log; /** * Uses flex's built in Object hashtable to do easier (and hopefully faster) lookups for an item in a collection based on a keyField (like companyId). */ public class ListIndexer extends EventDispatcher implements IMXMLObject { private static var log : ILogger = Log.getLogger("pathways.util.ListIndexer"); private var _source : IList; private var _keyField : String = "id"; private var internalKeyIndex : Object; //Contains the actual key -> position index hashmap private var keyList : Array; //A list of keys in order. To make finding the old key quicker when doing updates public function ListIndexer(source : IList = null, keyField : String = "id") { log.info("Instantiating"); this.keyField = keyField; this.source = source; } public function initialized(document : Object, id : String) : void { } [Bindable(event="keyFieldChanged")] public function get keyField() : String { return _keyField; } public function set keyField(v : String) : void { if (v != _keyField) { log.debug("set keyfield to " + v); _keyField = v; reIndex(); //Reindex } } [Bindable(event="sourceChanged")] public function get source() : IList { return _source; } public function set source(v : IList) : void { if (_source != v) { if (_source) { //Make sure we remove the old listener _source.removeEventListener(CollectionEvent.COLLECTION_CHANGE, sourceUpdated); } log.debug("Setting source to " + getQualifiedClassName(v) + " " + v); _source = v; //Initial index reIndex(); //Using a weak event listener _source.addEventListener(CollectionEvent.COLLECTION_CHANGE, sourceUpdated, false, 0, true); dispatchEvent(new Event("sourceChanged")); } } private function sourceUpdated(event : CollectionEvent) : void { log.debug("Collection event! " + event.type + ", kind = " + event.kind); switch(event.kind) { case CollectionEventKind.REFRESH: case CollectionEventKind.RESET: reIndex(); break; case CollectionEventKind.UPDATE: handleChangeEvents(event.items); case CollectionEventKind.REPLACE: itemsReplaced(event.location, event.items.length); break; case CollectionEventKind.ADD: addItems(event.location, event.items); break; case CollectionEventKind.REMOVE: removeItems(event.location, event.items.length); case CollectionEventKind.MOVE: moveItem(event.oldLocation, event.location); default : log.error("I don't know how to handle collection event of kind " + event.kind); } } /** * Handles change events. Somewhat smart, but not genius. Probably could use some testing too :) */ protected function handleChangeEvents(events : Array) : void { log.debug("Looking though " + events.length + " PropertyChangeEvents"); var updatedIndexes : Array = []; eachEvent: for (var i : Number = 0; i < events.length; i++) { var changeEvent : PropertyChangeEvent = events[i]; //Is it our key field? If not, we don't care :) if (changeEvent.property != keyField) continue; //Key field was changed. //Find the item (and index) that was updated var updatedIndex : Number; var updatedItem : Object = null; if (changeEvent.target && (updatedIndex = source.getItemIndex(changeEvent.target)) != -1) { updatedItem = changeEvent.target; } else if (changeEvent.source && (updatedIndex = source.getItemIndex(changeEvent.source)) != -1) { updatedItem = changeEvent.source; } else { log.warn("Received a change event for an item I can't find in source. Shouldn't see this."); continue; } //Check to see if the index is already in our list, don't wanna reindex it twice for each (var alreadyIndexing : Number in updatedIndexes) if (updatedIndex == alreadyIndexing) continue eachEvent; updatedIndexes.push(updatedIndex); } log.debug("Turns out, " + updatedIndexes.length + " actual items have changed that we care about"); //Now do the boring old re-index on each item we have identified for each (updatedIndex in updatedIndexes) itemUpdated(updatedIndex); } /** * Item moved - THIS NEEDS SOME TESTING I THINK!!! */ protected function moveItem(oldIndex : Number, newIndex : Number) : void { //Sanity check - remove when we've tested this if(extractKeyFromItem(_source.getItemAt(newIndex), newIndex) != keyList[oldIndex]) throw "Failed sanity check. The item we're getting from newIndex " + newIndex + " doesn't have the key we got from oldIndex " + oldIndex; //Get the key for our moved Item var movedKey : String = keyList[oldIndex]; //TODO: Test the *shit* out of this funky logic. NFI the best way if (oldIndex > newIndex) { //Moved left towards 0. //The keys that used to be at #newIndex through #(oldIndex - 1) all need a +1 to their index number (they move right to accomodate the value) for (var i : Number = newIndex; i < oldIndex; i++) internalKeyIndex[keyList[i]]++; } else { //Moved right away from 0 //Keys that used to be at #oldIndex(+1) through #(newIndex) need a -1 to their index value (they move left to fill the gap) for (i = oldIndex + 1; i <= newIndex; i++) internalKeyIndex[keyList[i]]--; } //Update the index for the actual moved item while we're at it internalKeyIndex[movedKey] = newIndex; //Now simply adjust the keyList by //first, removing from oldIndex... keyList.splice(oldIndex, 1); //then inserting/adding it at newIndex if (newIndex < keyList.length) //Insert keyList.splice(newIndex, 0, movedKey); else //Add keyList[keyList.length] = movedKey; } /** * Items have been removed from the source, dump them from our index */ protected function removeItems(index : Number, itemCount : Number) : void { //The first valid index after those we're going to delete var firstValidIndex : Number = index + itemCount; //Get list of keys to delete var keysToDelete : Array = []; for (var i : Number = index; i < firstValidIndex; i++) keysToDelete.push(keyList[i]); //Get list of keys for which the index needs to be -= itemCount var keysToUpdate : Array = []; for (i = firstValidIndex; i < keyList.length; i++) keysToUpdate.push(keyList[i]); //Purge keys from keyList keyList.splice(index, itemCount); //Delete purged keys from keyCache for each (var key : String in keysToDelete) delete internalKeyIndex[key]; //Update the index that post-delete keys point to. It's now off by itemCount. for each (key in keysToUpdate) internalKeyIndex[key] -= itemCount } /** * New Items have been added to the source, so we need to index them */ protected function addItems(index : Number, items : Array) : void { log.debug("Add " + items.length + " at position " + index); var oldLength : Number = keyList.length; //Just for debugging //Maybe we got off lucky? if (index == keyList.length) { //Added to end of list, yay! } else { //Update the index value for any keys that are past the add index for(var j : Number = index; j < keyList.length; j++) internalKeyIndex[keyList[j]] += items.length; //Make space for the new keys in the keyList keyList.splice(index, 0, new Array(items.length)); } //Actually do our adding, checking for dupes for (var i : Number = 0; i < items.length; i++) { var key : String = extractKeyFromItem(items[i], index + i); if (key in internalKeyIndex) throw new Error("New item #" + (index + i) + " has duplicate key (" + key + ")"); else addKey(key, index + i); } log.debug("Added " + items.length + " items, went from " + oldLength + " to " + keyList.length); } /** * The item at the indicated position has been replaced or updated * Will need modifying if we allow duplicate keys in future. */ protected function itemUpdated(position : Number) : void { log.debug("Item at position #" + position + " has been changed."); //Get the new object at the position var newItem : Object = _source.getItemAt(position); //Get the new key for the new object var newKey : String = extractKeyFromItem(newItem, position); //If we're lucky the old and new keys will be the same, which means we can return quickly as our index is still good if(newKey in internalKeyIndex) { //Key exists. Is it the same position? if (internalKeyIndex[newKey] == position) { log.debug("New item has same key, so our index is intact"); return; //Nothing to do } //Nope, we must have a duplicate key //TODO: update this if we allow dupe keys throw new Error("New item #" + position + " has duplicate key (" + newKey + ")"); } //If we're here, then we have a new key, and an old one is no longer valid. //Find the old key var oldKey : String = keyList[position]; replaceKey(position, oldKey, newKey); log.debug("New key indexed"); } /** * Multiple items have been updated. */ protected function itemsReplaced(position : Number, itemCount : Number) : void { for (var i : Number = 0; i < itemCount; i++) itemUpdated(position + i); } /** * Index the whole collection */ public function reIndex() : void { if (!source) return; if (!keyField) return; log.debug("Re-indexing entire source, looking for keyField " + keyField); //Splat our index and keylist internalKeyIndex = {}; keyList = []; var idx : Number = -1; for each (var item : Object in _source) { ++idx; var key : String = extractKeyFromItem(item, idx); //TODO - add flags for whether or not these are allowed if (key in internalKeyIndex) throw new Error("item[" + idx + "] in source collection has a duplicate " + keyField + " (" + key + ")"); //Put it in our index. addKey(key, idx); } log.debug((idx + 1) + " objects indexed successfully"); } /** * Puts the key into our index with the position value */ private function addKey(key : String, idx : Number) : void { internalKeyIndex[key] = idx; //Search index keyList[idx] = key; //backwards index to aid in removing old keys on updates } /** * Removes an old key and replaces it with a new key pointing to the same index */ private function replaceKey(index : Number, oldKey : String, newKey : String) : void { delete internalKeyIndex[oldKey]; //Do it first just in case we get oldkey == newkey. Shouldn't happen, but there's no need to do this after addKey. addKey(newKey, index); } /** * Removes a key from indexes */ /* private function deleteKey(key : String, idx : Number) : void { keyList.splice(idx, 1); delete internalKeyIndex[key]; } */ /** * Returns the vetted key from an object. idx is only to aid in error messages */ private function extractKeyFromItem(item : Object, idx : Number) : String { //TODO - add flags for whether or not these are allowed if (!keyField in item) throw new Error("keyField \"" + keyField + " not found in item #" + idx); //TODO - add flags for whether or not these are allowed - will require a magic key value - can't be null! if (!item[keyField]) throw new Error("item[" + idx + "] in source collection has a null or undefined " + keyField); //Get our string key var key : String = "" + item[keyField].toString(); if (key == "") throw new Error("Can't search for a blank key"); return key; } /** * Returns a usable string key or throws an error complaining about it. */ private function validateSearchKey(searchKey : Object) : String { //TODO - add flags for whether or not these are allowed - will require a magic key value - can't be null! if (!searchKey) throw new Error("Can't search for a null key"); var key : String = ""; if (searchKey) key += searchKey.toString(); if (key == "") throw new Error("Can't search for a blank key"); return key; } /** * Returns true if an object with that key exists */ public function keyExists(searchKey : Object) : Boolean { var validKey : String = validateSearchKey(searchKey); return (validKey in internalKeyIndex); } /** * Get position of object with specified key. * Will need modifying if we allow duplicate keys in future. */ public function getIndex(searchKey : Object) : Number { var validKey : String = validateSearchKey(searchKey); return validKey in internalKeyIndex ? internalKeyIndex[validKey] : -1; } } }