/*
* Copyright 2003-2010 Tufts University Licensed under the
* Educational Community 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.osedu.org/licenses/ECL-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 tufts.vue;
import tufts.Util;
import tufts.vue.ds.Schema;
import java.util.*;
import java.lang.ref.Reference;
import com.google.common.collect.Multimap;
import com.google.common.collect.Multimaps;
import com.google.common.collect.Iterators;
/**
* A general key/value map for storing values: e.g., meta-data.
*
* Uses an underlying Multimap impl, so multiple values for the same key are supported.
*
* The insertion order of each key/value is preserved, even for each use of
* the same key with different values.
*
* @version $Revision: 1.19 $ / $Date: 2010-02-03 19:17:41 $ / $Author: mike $
*/
public class MetaMap implements TableBag, XMLUnmarshalListener, tufts.vue.ds.Relation.Scannable
{
private static final org.apache.log4j.Logger Log = org.apache.log4j.Logger.getLogger(MetaMap.class);
// Note: LinkedHashMultimap preserves the order of all "KEY+VALUE" additions, not just the KEY --
// this is good for preserving semantics of incoming data, as the order is often good information
// (especially if the source is XML data). We could potentially hack this impl to even preserve
// some kind of decoration that allows duplicate key-value pairs (good for XML again).
// e.g.: wrap the value in something that disguises it.
// Performance: allow only creating a collection when the second value for
// a given key is added. Would need to either hack google collections for this,
// or do as part of our own complete re-implementation.
/** the backing Multimap: deliberately un-typed */
private final Multimap mData = Multimaps.newLinkedHashMultimap();
private volatile MapModel mTableModel;
private boolean mHoldingChanges = false;
private int mChanges;
private List<TableBag.Listener> listeners;
private static final Map<String,Key> KEY_MAP = new java.util.concurrent.ConcurrentHashMap();
private static final boolean KEY_DEBUG_MODE = false;
private Schema mSchema;
public MetaMap() {}
public void setSchema(Schema schema) {
//if (DEBUG.Enabled) Log.debug("setSchema " + schema);
//mSchema = Schema.lookup(schema);
// note: if a meta-map is ever persisted outside of an LWComponent, and it wants
// to use live schema references, we'll need to deal with Schema.lookup in here again.
mSchema = schema;
}
public Schema getSchema() {
//Log.debug("getSchema returns " + mSchema);
return mSchema;
}
// public int getSchemaID() {
// if (mSchema == null)
// return 0;
// else
// return mSchema.getMapLocalID();
// }
@Override
public boolean equals(Object o) {
if (o instanceof MetaMap == false)
return false;
return mData.equals( ((MetaMap)o).mData );
}
/**
*
* A decorator for String that will be object equivalent with other Key String decorators
* regardless of case. This permits *preserving* case within a given key, while ignoring it
* for hash/equals.
*
* We could support object equivalence with actual Strings, but the deep down underlying
* java.util.HashMap these keys are ultimately destined for doesn't do key compairson in the
* order that would support that (e.g., we'd need Key.equals(String), it does
* String.equals(Object))
*
*/
// todo: would be more performant to just have the Field class have a getKey as well
// as a getName, and the Field.key can always be a lower-cased value. Then, all
// lookups can just be forced to lower-case, and we can skip the hashing & key
// creation on new mixed-case key-based lookups. Tho we'd still need to deal with
// Resource MetaMap's... I suppose we could create a special Resource schema
// with all the fields that ever appear in any resource. Actually, better to
// create one schema per OSID. Tho only load with stuff that's actually been
// put on a map -- don't auto-load from every search result ever seen.
// -- We'd also have to deal with restoring the Field.key <-> MetaMap relationships
// when deserializing, as now it can just used the saved string values.
private static class Key {
final String name;
final int hash;
private Key(String s) {
name = s;
hash = name.toLowerCase().hashCode();
}
@Override
public boolean equals(Object o) {
if (KEY_DEBUG_MODE && DEBUG.META)
Log.debug(String.format("testing [%s]=[%s]", Util.tags(this), Util.tags(o)));
if (o == this)
return true;
if (o.getClass() != Key.class) {
Log.warn("IS NOT KEY: " + Util.tags(o), new Throwable("HERE"));
return false;
}
if (KEY_DEBUG_MODE && DEBUG.Enabled) {
final boolean eq = name.equalsIgnoreCase( ((Key)o).name );
if (eq && !name.equals(o.toString()))
Log.debug(String.format("matched mixed-case [%s] to [%s]", name, o));
return eq;
} else {
return name.equalsIgnoreCase( ((Key)o).name );
}
}
@Override
public int hashCode() {
return hash;
}
@Override
public String toString() {
return name;
}
// private static Object instance(String s) {
// // old style impl: regular Strings as keys: all MetaMap.Key code ignored
// return s;
// }
private static Key instance(String s) {
// case independent key impl: all Key's cached and only
// Key objects used in the Multimap, and all lookups require
// an additional hash lookup for a Key object, or the creation
// of a new one.
final Key existing = KEY_MAP.get(s);
if (existing != null)
return existing;
final Key newKey = new Key(s);
KEY_MAP.put(s, newKey);
if (KEY_DEBUG_MODE) Log.debug("*** CREATED KEY [" + newKey + "]");
return newKey;
}
}
/** add the given key/value pair */
public synchronized void put(final String key, final Object value)
{
final Object internalKey = Key.instance(key);
if (DEBUG.DATA) Log.debug("put " + internalKey + " " + Util.tags(value));
if (mData.put(internalKey, value))
markChange();
}
/** will ensure the given key has ONLY the given value: any other values present will be removed */
public synchronized void set(final String key, final Object value)
{
final Key internalKey = Key.instance(key);
//mData.replaceValues(internalKey, Util.iterable(value));
final Collection bag = mData.get(internalKey);
if (bag.isEmpty()) {
bag.add(value);
} else {
if (DEBUG.DATA)
Log.debug("replacing " + internalKey + " " + Util.tags(bag) + " with " + Util.tags(value));
//+ "; got=" + Util.tags(getFirst(internalKey)));
bag.clear();
bag.add(value);
}
markChange();
}
/** Will add the String values of all key/value pairs */
public synchronized void putAllStrings(final Iterable<Map.Entry> entries) {
final boolean onHold = mHoldingChanges;
if (!onHold)
holdChanges();
for (Map.Entry e : entries)
put(e.getKey().toString(), e.getValue().toString());
if (!onHold)
releaseChanges();
}
/** backward compat for now: same as put */
public void add(final String key, final Object value) {
put(key, value);
}
/** @return true if the given value is considered "empty", e.g., a null,
* 0 length string, or zero size collection */
private static boolean isEmpty(Object v) {
if (v == null)
return true;
else if (v.getClass() == String.class && v.toString().length() == 0)
return true;
else if (v instanceof Collection && ((Collection)v).size() == 0)
return true;
else
return false;
}
/** add the given key/value, only if the value is considered "non-empty"
* Empty values currently include null, zero length Strings, and empty Collections */
public void putNonEmpty(String key, Object value) {
if (!isEmpty(value))
put(key, value);
//else if (DEBUG.DATA) Log.debug("EMPTY: " + Util.tags(value));
}
/** set the given key/value, only if the value is considered "non-empty" */
public void setNonEmpty(String key, Object value) {
if (!isEmpty(value))
set(key, value);
//else if (DEBUG.DATA) Log.debug("EMPTY: " + Util.tags(value));
}
/** remove ALL values having the given key */
public synchronized Object remove(Object key) {
final Object prior = mData.removeAll(key);
if (prior != null)
markChange();
return prior;
}
private static Object extractFirstValue(Object o) {
//Log.debug("extracing 1st value from: " + Util.tags(o));
if (o instanceof Collection) {
// In practice, this is the case that always occurrs with the current google
// collections impl -- we usually see an instance of:
// com.google.common.collect.StandardMultimap$WrappedSet
// Todo performance: this is a terrible waste much of the time. When there
// are no repeat values for a given key, we must construct and run an
// iterator just to extract the single value. Hope for an optimized version
// of Multimaps from google that automatically handle this. SMF 2008-12-03
//Log.debug("extracting from Collection " + Util.tags(o));
final Collection bag = (Collection) o;
if (bag.isEmpty())
return null;
else
return bag.iterator().next();
}
// // placed here this case will never be used: all instances of List are instances of Collection
// else if (o instanceof List) {
// //Log.debug("extracting from List " + Util.tags(o));
// final List list = (List) o;
// return list.isEmpty() ? null : list.get(0);
// }
else {
if (DEBUG.Enabled) Log.debug("extracing 1st value as is (no collection): " + Util.tags(o));
return o;
}
}
public Object get(String key) {
return getFirst(key); // Key.instance?
}
public Collection<String> getValues(String key) {
return mData.get(Key.instance(key));
}
/** @return the property value for the given key, dereferened if an instanceof java.lang.ref.Reference is found */
public Object getValue(String key) {
Object o = getFirst(key);
if (o instanceof Reference) {
o = ((Reference)o).get();
if (o == null) {
if (DEBUG.Enabled) Log.debug("value was GC'd for key: " + key);
//mData.remove(Key.instance(key), o); // don't need to see it again
}
}
return o;
}
/** @return the first value found for the given key */
public synchronized Object getFirst(String key) {
return getFirst(Key.instance(key));
}
// TODO: consider using a CharSequence as the key argument
// everywhere, which will accept Strings, but would allow for any
// Key object of our design that also implements that (and is
// convertable via toString())
private synchronized Object getFirst(Key key) {
return extractFirstValue(mData.get(key));
}
/** @return first value found for key as a string */
public String getFirstString(String key) {
final Object o = getFirst(key);
return o == null ? null : o.toString();
}
/** @return first value found for key as a string: current impl identical to getFirstString */
// if there are multiple values for the given key, this someday could return a string
// that concatenates all the values with a delimiter
public String getString(String key) {
return getFirstString(key);
}
// TODO: make return types (Object v.s String) more consistent, or more clear as to result
// public String getAny(String... keys) {
// // todo: could also handle some case hacking until we support case independent keys
// for (String k : keys) {
// final String value = getFirstValue(k);
// if (value != null)
// return value;
// }
// return null;
// }
/** TableBag impl */
public int size() {
return mData.size();
}
public boolean hasKey(String key) {
// can we optimize if the key-cache finds to key at all to look up?
// I think only if we also case-fold the key-cache.
return mData.containsKey(Key.instance(key));
//return mData.containsKey(key);
}
public boolean hasEntry(String key, CharSequence value) {
return mData.containsEntry(Key.instance(key), value);
//return mData.containsEntry(key, value);
}
// public Set keySet() {
// return mData.keySet();
// }
// public Map<String,?> asMap() {
// return mData.asMap();
// }
// /** @return a set of entries that presents a flattened view of all key-value pairs */
// public Collection<Map.Entry<String,Object>> entrySet() {
// //return mData.entries();
// // here we need the a flattening Collection forwarder, or
// // replace entrySet with a special flattening iterator
// //return mData.entrySet();
// }
// private Collection<Map.Entry<String,Collection<?>>> collectionEntrySet() {
// return mData.asMap().entrySet();
// }
/** @return a flat collection of key-value pairs -- modifications to the collection will modify the map */
//public Collection<Map.Entry<Key,Object>> entries() {
public Collection<Map.Entry> entries() {
return mData.entries();
}
public Collection values() {
return mData.values();
}
private Collection<Map.Entry<String,Object>> mPersistEntries;
/** for castor persistance of key/value pairs -- returns mappable PropertyEntry instances */
//public Iterator<Map.Entry<String,Object>> iterateEntries() // could try castor iterate/add patttern
public Collection<Map.Entry<String,Object>> getPersistEntries() {
if (mPersistEntries == null)
mPersistEntries = new ArrayList();
if (mData.size() > 0) {
mPersistEntries.clear();
if (DEBUG.XML) Log.debug("LOADING ENTRIES n=" + mData.size() + "; in " + Util.tag(this));
// NOTE: at runtime, we sometimes put the actual object in the property map
// E.g., URLResource will put a URI in the Resource properties for
// @file.relative -- which has worked for us as it persists the toString()
// version. But on deserialization, a Multimap will start with the String
// only version of @file.relative, and then add the object version,
// resulting in the value appearing in there twice (both are normally hidden
// except for debug tho). But when saved again, @file.relative will then
// appear in the save file twice -- once for the original String, once for
// the current runtime URI, stringified at persist time. This is okay -- on
// deserialization again, they'll both be encoundered as Strings again and
// be combined into a single entry. The one problem this could potentially
// cause is if our code were to ever rely on finding the original URI object
// in the resource properties. This currently isn't done -- we only look for
// String values, and the original object is in there only for debugging.
// Just be aware of this.
//
// [ update: Resource setProperty now uses MetaMap.set v.s. MetaMap.put,
// so repeats should no longer be possible ]
for (Map.Entry e : entries())
mPersistEntries.add(new PropertyEntry(e));
}
return mPersistEntries;
}
public void XML_initialized(Object context) {}
public void XML_completed(Object context) {
if (DEBUG.XML) Log.debug("UNPACKING ENTRIES IN " + this + " context=" + context + "; persistEntries: " + Util.tags(mPersistEntries));
if (mPersistEntries == null)
return;
for (Map.Entry<String,?> e : mPersistEntries) {
if (DEBUG.XML) Log.debug("UNPACKING " + e);
// NOTE: We could use String.intern() for the keys. This is a debatable
// performance issue -- use of intern() will slow down all future String
// creations. But if we don't do this or something like it, every key
// object is a unique String instance after deserializing, which can be a
// ton of needless objects when spread across hundreds of meta-maps, each
// with dozens of keys. A beter impl would probably be to have a static
// MetaMap internal global key hash.
// put(e.getKey().intern(), e.getValue());
put(e.getKey(), e.getValue());
}
if (DEBUG.XML) Log.debug("UNPACKED " + this);
//if (DEBUG.Enabled) Log.debug("XML COMPLETED DATA SET w/SCHEMA: " + getSchema());
}
public void XML_fieldAdded(Object context, String name, Object child) {}
public void XML_addNotify(Object context, String name, Object parent) {}
// // public Iterable<Map.Entry<String,Object>> entries() {
// // return mData.entries();
// // }
// // public Util.Itering<Map.Entry<String,?>> entries() {
// // return new FlatteningIterator(mData);
// // }
// private static Util.Itering<Map.Entry<String,?>> entries(Map<String,Collection> map) {
// return new FlatteningIterator(map);
// }
// /** Flatten's a Map who's values are collections: returns a key/value for each value in each collection */
// private static final class FlatteningIterator<K,V> extends Util.AbstractItering<Map.Entry<K,V>> {
// final Iterator<Map.Entry<Object,Collection>> keyIterator;
// /** this object returned as the result every time */
// final KVEntry entry = new KVEntry();
// Iterator valueIterator;
// public FlatteningIterator(Map<Object,Collection> map) {
// keyIterator = map.entrySet().iterator();
// if (keyIterator.hasNext()) {
// findValueIteratorAndKey();
// } else {
// valueIterator = Iterators.emptyIterator();
// }
// }
// void findValueIteratorAndKey() {
// final Map.Entry e = keyIterator.next();
// entry.key = e.getKey();
// Collection collection = (Collection) e.getValue();
// valueIterator = collection.iterator();
// }
// public boolean hasNext() {
// return valueIterator.hasNext() || keyIterator.hasNext();
// }
// /** note: current impl will always return the same Map.Entry object */
// public Map.Entry next() {
// if (!hasNext()) throw new NoSuchElementException();
// if (!valueIterator.hasNext())
// findValueIteratorAndKey();
// entry.value = valueIterator.next();
// return entry;
// }
// }
private void markChange() {
if (mHoldingChanges)
mChanges++;
else if (mTableModel != null)
mTableModel.reload();
}
/** No listeners will be updated until releaseChanges is called. Multiple
* overlapping holds are okay. */
public synchronized void holdChanges() {
if (DEBUG.RESOURCE && DEBUG.META) out("holding changes");
mHoldingChanges = true;
}
/**
* If any changes made since holdChanges, listeners will be notified.
* It is crucial this is called at some point after holdChanges,
* or listeners may never get updated. Tho ideally only one call to
* this per holdChanges is made, Extra calls to this are okay
* -- it will at worst degrade update performance with rapid updates.
*/
public synchronized void releaseChanges() {
mHoldingChanges = false;
if (mChanges > 0 && mTableModel != null) {
if (DEBUG.RESOURCE && DEBUG.IMAGE) out("releasing changes " + mChanges);
mTableModel.reload();
}
if (DEBUG.RESOURCE && DEBUG.IMAGE) out("released changes " + mChanges);
mChanges = 0;
}
public synchronized java.util.Properties asProperties() {
final java.util.Properties props = new java.util.Properties();
for (Map.Entry e : entries())
props.put(e.getKey().toString(), e.getValue().toString());
return props;
}
/* Do NOT synchronize getTableModel with the rest of the methods: this is because
* when listeners get notified, one of the first things they're likely to do is ask
* for the table model, but if two different threads are active on the other end,
* we'll dead-lock. (E.g., an ImageLoader thread and the AWT thread (via a user
* LWSelection change) have both changed the current resource selection, and so the
* meta-data pane is doing two synchronized updates (one from each thread) back to
* back, but during one of the updates, it has to call back into the PropertyMap
* here, which may already be locked on one of the threads, because it was from
* there that a propertyMapChange was called.
*
* Eventually, the table model will probably want to move to the GUI and we can
* avoid this special case.
*/
// todo: this doesn't need to be so complicated as getTableModel()
// anymore: could replace with a getEntries() that returns a new
// array of all entries, fully iterable in a thread-safe manner,
// and non-modifiable.
public javax.swing.table.TableModel getTableModel() {
// mTableModel is volatile, so as of Java 1.5, the double-checked locking
// idiom used here should work.
if (mTableModel == null) {
synchronized (mData) { // sync on anything other than "this" just in case
if (mTableModel == null)
mTableModel = new MapModel();
}
}
return mTableModel;
}
public synchronized void addListener(TableBag.Listener l) {
if (listeners == null)
listeners = new java.util.ArrayList();
listeners.add(l);
}
public synchronized void removeListener(TableBag.Listener l) {
// Note: we can DEADLOCK here in AWT obtaining the method entry lock if this
// MetaMap instance is already locked by an ImageLoader thread.
if (listeners != null)
listeners.remove(l);
}
private static int notifyCount = 0;
public synchronized void notifyListeners()
{
if (listeners != null && listeners.size() > 0) {
notifyCount++;
if (DEBUG.RESOURCE || DEBUG.THREAD) out("notifyListeners " + listeners.size() + " of " + super.toString());
Iterator i = listeners.iterator();
while (i.hasNext()) {
Listener l = (Listener) i.next();
if (DEBUG.RESOURCE || DEBUG.THREAD) out("notifying: " + tufts.vue.gui.GUI.namex(l));
// Note: we can DEADLOCK in MetaDataPane.tableBagChanged (if it's
// synchronized) waiting against AWT which is already locking
// MetaDataPane, and waiting to lock this object (locked by an
// ImageLoader thread) to enter removeListener above.
l.tableBagChanged(this);
}
if (DEBUG.RESOURCE || DEBUG.THREAD) out("notifyListeners completed " + listeners.size());
}
}
private void out(Object o) {
Log.debug(String.format("@%x (#%d): %s",
System.identityHashCode(this),
notifyCount,
(o==null?"null":o.toString())));
}
@Override
public String toString() {
//return String.format("MetaMap@%x[n=%d %s]", System.identityHashCode(this), size(), mData);
try {
Schema s = getSchema();
String t = "";
if (s != null) {
String keyFieldName = "";
try { keyFieldName = s.getKeyFieldName(); }
catch (Throwable tx) { keyFieldName = "["+tx+"]"; }
t = String.format(" %s.%s=%s",
s.getName(),
s.getKeyFieldName(),
Util.tags(Util.maxDisplay(getString(keyFieldName), 50)));
}
return String.format("MetaMap@%06x[n=%d%s]",
System.identityHashCode(this),
size(),
t);
} catch (Throwable t) {
//t.printStackTrace();
return Util.tag(this) + "[" + t + "]";
}
}
@Override
public MetaMap clone() {
final MetaMap clone = new MetaMap();
clone.mData.putAll(mData);
clone.mSchema = mSchema;
return clone;
}
private class MapModel extends javax.swing.table.AbstractTableModel {
// TODO: we can get get rid of this now -- we're not doing the sort any more, as
// the order is being preserved in the data-map itself. The MetaDataPane viewer
// can provide differently sorted views on its own if it likes. HOWEVER, we may
// still want to keep some delegate like this to prevent concurrent mod
// exceptions while iterating in the MetaDataPane in case of changes while
// loading (e.g., additional image data has arrived) We may even want to leave
// the table model in case we ever want to show this in a table,
private final ArrayList<Map.Entry> mEntries = new ArrayList();
MapModel() {
if (DEBUG.RESOURCE) out("new " + getClass());
reload();
}
private void reload() {
mEntries.clear();
mEntries.addAll(MetaMap.this.entries());
if (DEBUG.RESOURCE) out("MapModel reload " + mEntries.size() + " items");
// final int totalRows = MetaMap.this.size(); // one row per key: includes repeated key's
// if (mEntries == null || mEntries.length != totalRows)
// mEntries = new KVEntry[totalRows];
// for (Map.Entry<String,?> e : MetaMap.this.entries())
// mEntries[ei++] = new KVEntry(e);
// // Arrays.sort(mEntries, new Comparator() {
// // public int compare(Object o1, Object o2) {
// // String k1 = (String) ((Map.Entry)o1).getKey();
// // String k2 = (String) ((Map.Entry)o2).getKey();
// // return k1.compareTo(k2);
// // }});
if (DEBUG.RESOURCE) {
out("loaded " + mEntries.size() + " entries");
if (DEBUG.META)
out("model loaded " + mEntries);
}
if (DEBUG.RESOURCE || DEBUG.THREAD) out("fireTableDataChanged...");
fireTableDataChanged();
MetaMap.this.notifyListeners();
}
public int getRowCount() {
return mEntries.size();
}
public Object getValueAt(int row, int col) {
// if (row >= mEntries.size()) {
// Util.printStackTrace(getClass() + " has only " + mEntries.size() + " entries, attempt to access row " + row);
// return "<empty>";
// }
final Map.Entry entry = mEntries.get(row);
if (entry == null) {
Log.warn(getClass().getName(), new Throwable("FYI: null entry at row " + row + "; col-request=" + col));
return null;
} else if (col == 0)
return entry.getKey();
//return String.format("%s@%x", entry.getKey(), System.identityHashCode(entry.getKey()));
else
return entry.getValue();
}
public int getColumnCount() { return 2; }
public String getColumnName(int col) {
//return col == 0 ? "Field" : "Value";
return null;
}
public Class getColumnClass(int colIndex) {
return colIndex == 0 ? String.class : Object.class;
}
}
}
// TODO: allow specifing a prefix, and maybe some special handling for
// nesting MetaMap's hierarchically. Would still need a flattened
// outer decoration of all the contained property maps, with combined
// dotted keys. This would be perfect for mapping XML data.
// TODO: handle case-independence in keys. An easy impl would create a ton of key
// objects instead of using String's as keys. An ideal impl would provavly involve
// hacking google collections to allow the use of an underlying case-independent
// hash-set impl (to be written) instead of java.util.HashSet
// What if we had an introspecting PropertyMap impl? (or Map impl, or Multimap impl, or
// whatever) could specify which fields you want bound to what to use just a sub-set
// (e.g., LWComponent properties) Or in meantime, could just make use of our existing
// LWComponent.Key class to make it even easier. Might NOT want that to just be called
// the "node." sub-set: maybe something more semantic like "ui." or "display."
// TODO: this type of map should not be used for internal runtime properties that want
// to be set and re-set, for possibly including SoftReference'd properties, etc -- to
// switch to this impl, would need to add a class similar to old PropertyMap (w/out the
// addProperty / unique key stuff, e.g., a DataMap), but with the special Reference
// handling, Class property handling, etc. Ideally have it impl a ClientData interface
// that any object can impl to use to pass through to the DataMap (class Resouce and
// class LWComponent could even subclass this, as their currently top level objects)
// TODO: may want to create this as a generic with Object as key (and maybe even value
// as Object v.s. a Collection, or some wrapper class that handles
// collection/singleton/meta-map), and handle in that any hierarchy encapsulation we end
// up wanting: e.g., a value could actually be another meta-map, and the flatting
// iterator could build up the key each time it descends. Would make for very noisy
// persistance (fat keys) when flattened then serialized, tho would ideally create a
// specialized castor mapping that would beautifully handle the nesting, (hopefully
// castor's limitiations wouldn't get in our way).
// Don't forget that the main reason to using hashing collections for all of this is not
// for presenting a sorted order (that could be produced once), but to provide fast
// hashed lookups / searches / filters - tho currently all our searches are value based,
// we'll be adding keyword based searches. (e.g. styling) E.g., a TreeMap is not
// only a map that maintains sorted order, but uses the red-black tree as a hashing
// looked mechanism based on the searched for key. Still, technically, I suppose
// if you know a "done/loaded" time for your collection, at which you could perform
// your sort once, you could use a LinkedHashMap all the way through, just
// performing the sort on it once (or a TreeMap to start, switching over to LinkedHashMap
// later) Has anyone created a crazy run-time-polymorphic collection set such as this?