tufts.vue.PropertyMap.java Source code

Java tutorial

Introduction

Here is the source code for tufts.vue.PropertyMap.java

Source

/*
* 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;
        }

    }
}