/* * 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 java.util.*; import java.lang.ref.*; /** * @deprecated -- replaced by MetaMap Nov 2008 * * A general HashMap for storing property values: e.g., meta-data. * * @version $Revision: 1.28 $ / $Date: 2010-02-03 19:17:41 $ / $Author: mike $ */ public class PropertyMap extends java.util.HashMap<String,Object> implements TableBag { private static final org.apache.log4j.Logger Log = org.apache.log4j.Logger.getLogger(PropertyMap.class); // public interface Listener { // void propertyMapChanged(PropertyMap p); // } private volatile SortedMapModel mTableModel; private Object mTableModel_LOCK = new Object(); private boolean mHoldingChanges = false; private int mChanges; private List listeners; private static final String NULL_MASK = "(empty)"; public PropertyMap() {} @Override public synchronized Object put(String k, Object v) { // TODO: we want to *preserve* case for display, but not differentiate based on it... //if (k instanceof String) k = ((String)k).toLowerCase(); // if (v instanceof String) //v = org.apache.commons.lang.StringEscapeUtils.unescapeHtml((String)v); final Object prior = super.put(k, v == null ? NULL_MASK : v); // todo: this a bit overkill: could have a higher level // trigger for this, instead of triggering any table listeners // every time. Our hold/release deals with specific batch // loads, which is good enough for now. if (mHoldingChanges) mChanges++; else if (mTableModel != null) mTableModel.reload(); return prior; } private static boolean hasContent(Object v) { if (v == null) return false; else if (v.getClass() == String.class && v.toString().length() == 0) return false; else if (v instanceof Collection && ((Collection)v).size() == 0) return false; else return true; } private Object putIfContent(String k, Object v) { return hasContent(v) ? put(k, v) : null; } @Override public synchronized Object remove(Object key) { final Object prior = super.remove(key); if (prior != null) { if (mHoldingChanges) mChanges++; else if (mTableModel != null) mTableModel.reload(); } return prior; } public synchronized Object get(Object k) { return super.get(k); } /** @return the property value for the given key, dereferened if an instanceof java.lang.ref.Reference is found */ public Object getValue(Object k) { Object o = get(k); if (o instanceof Reference) { o = ((Reference)o).get(); if (DEBUG.Enabled && o == null) Log.debug("value was GC'd for key: " + k); } return o; } public String getProperty(Object key) { Object v = get(key); return v == null ? null : v.toString(); } /** Set the value for the given key, overwriting any existing value. */ public void setProperty(String key, String value) { put(key, value); } /** * Add a property with the given key. If a key already exists * with this name, the key will be modified with an index. * @return - the key actually created */ public synchronized String addProperty(final String desiredKey, Object value) { String key = desiredKey; int index = 1; while (containsKey(key)) key = String.format("%s.%03d", desiredKey, index++); put(key, value); return key; } public synchronized String addIfContent(final String desiredKey, Object value) { if (hasContent(value)) return addProperty(desiredKey, value); else return null; } /** 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() { java.util.Properties props = new java.util.Properties(); // todo: this not totally safe: values may not all be strings props.putAll(this); 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. */ 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 (mTableModel_LOCK) { if (mTableModel == null) mTableModel = new SortedMapModel(); } } return mTableModel; } public synchronized void addListener(Listener l) { if (listeners == null) listeners = new java.util.ArrayList(); listeners.add(l); } public synchronized void removeListener(Listener l) { // Note: we can DEADLOCK here in AWT obtaining the method entry lock if this // PropertyMap 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.propertyMapChanged (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()))); } public String toString() { return "PropertyMap@" + Integer.toHexString(hashCode()) + super.toString(); } public int hashCode() { return mTableModel == null ? 0 : mTableModel.hashCode(); // doesn't change depending on contents } @Override public PropertyMap clone() { final PropertyMap clone = (PropertyMap) super.clone(); clone.listeners = null; clone.mChanges = 0; clone.mTableModel = null; return clone; } private static class Entry implements Comparable { final String key; final Object value; final boolean priority; Entry(Map.Entry e, boolean priority) { this.key = tufts.Util.upperCaseWords((String) e.getKey()); this.value = e.getValue(); this.priority = priority; } Entry(Map.Entry e) { this(e, false); } public int compareTo(Object o) { if (priority) return Short.MIN_VALUE; else if (((Entry)o).priority) return Short.MAX_VALUE; else return key.compareTo(((Entry)o).key); } public String toString() { return key + "=" + value; } } // TODO: move this out to viewer private class SortedMapModel extends javax.swing.table.AbstractTableModel { private Entry[] mEntries; SortedMapModel() { if (DEBUG.RESOURCE) out("new SortedMapModel"); reload(); } // make sure there is a sync on the HashMap before this is called private void reload() { mEntries = new Entry[PropertyMap.this.size()]; if (DEBUG.RESOURCE) out("SortedMapModel: reload " + mEntries.length + " items"); int ei = 0; for (Map.Entry<String,Object> e : entrySet()) { final String key = e.getKey().toLowerCase(); final boolean priority = key.equals("title") || key.equals("file") || key.equals("url") || key.equals("name") ; mEntries[ei++] = new Entry(e, priority); } Arrays.sort(mEntries); /* mEntries = (Map.Entry[]) PropertyMap.this.entrySet().toArray(new Map.Entry[size()]); 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.length + " entries"); if (DEBUG.META) out("model loaded " + Arrays.asList(mEntries)); } if (DEBUG.RESOURCE || DEBUG.THREAD) out("fireTableDataChanged..."); fireTableDataChanged(); notifyListeners(); } public int getRowCount() { return mEntries.length; } public Object getValueAt(int row, int col) { if (row > mEntries.length) { tufts.Util.printStackTrace("SortedMapModel has only " + mEntries.length + " entries, attempt to access row " + row); return "<empty>"; } final Entry entry = mEntries[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.key; else return entry.value; } 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; } } }