StringMap.java Source code

Java tutorial

Introduction

Here is the source code for StringMap.java

Source

// Copyright 2004-2005 Mort Bay Consulting Pty. Ltd.
// ------------------------------------------------------------------------
// Licensed under the Apache 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.apache.org/licenses/LICENSE-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.

import java.io.Externalizable;
import java.util.AbstractMap;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;

/* ------------------------------------------------------------ */
/** Map implementation Optimized for Strings keys..
 * This String Map has been optimized for mapping small sets of
 * Strings where the most frequently accessed Strings have been put to
 * the map first.
 *
 * It also has the benefit that it can look up entries by substring or
 * sections of char and byte arrays.  This can prevent many String
 * objects from being created just to look up in the map.
 *
 * This map is NOT synchronized.
 *
 * @author Greg Wilkins (gregw)
 */
public class StringMap extends AbstractMap implements Externalizable {
    public static final boolean CASE_INSENSTIVE = true;
    protected static final int __HASH_WIDTH = 17;

    /* ------------------------------------------------------------ */
    protected int _width = __HASH_WIDTH;
    protected Node _root = new Node();
    protected boolean _ignoreCase = false;
    protected NullEntry _nullEntry = null;
    protected Object _nullValue = null;
    protected HashSet _entrySet = new HashSet(3);
    protected Set _umEntrySet = Collections.unmodifiableSet(_entrySet);

    /* ------------------------------------------------------------ */
    /** Constructor. 
     */
    public StringMap() {
    }

    /* ------------------------------------------------------------ */
    /** Constructor. 
     * @param ignoreCase 
     */
    public StringMap(boolean ignoreCase) {
        this();
        _ignoreCase = ignoreCase;
    }

    /* ------------------------------------------------------------ */
    /** Constructor. 
     * @param ignoreCase 
     * @param width Width of hash tables, larger values are faster but
     * use more memory.
     */
    public StringMap(boolean ignoreCase, int width) {
        this();
        _ignoreCase = ignoreCase;
        _width = width;
    }

    /* ------------------------------------------------------------ */
    /** Set the ignoreCase attribute.
     * @param ic If true, the map is case insensitive for keys.
     */
    public void setIgnoreCase(boolean ic) {
        if (_root._children != null)
            throw new IllegalStateException("Must be set before first put");
        _ignoreCase = ic;
    }

    /* ------------------------------------------------------------ */
    public boolean isIgnoreCase() {
        return _ignoreCase;
    }

    /* ------------------------------------------------------------ */
    /** Set the hash width.
     * @param width Width of hash tables, larger values are faster but
     * use more memory.
     */
    public void setWidth(int width) {
        _width = width;
    }

    /* ------------------------------------------------------------ */
    public int getWidth() {
        return _width;
    }

    /* ------------------------------------------------------------ */
    public Object put(Object key, Object value) {
        if (key == null)
            return put(null, value);
        return put(key.toString(), value);
    }

    /* ------------------------------------------------------------ */
    public Object put(String key, Object value) {
        if (key == null) {
            Object oldValue = _nullValue;
            _nullValue = value;
            if (_nullEntry == null) {
                _nullEntry = new NullEntry();
                _entrySet.add(_nullEntry);
            }
            return oldValue;
        }

        Node node = _root;
        int ni = -1;
        Node prev = null;
        Node parent = null;

        // look for best match
        charLoop: for (int i = 0; i < key.length(); i++) {
            char c = key.charAt(i);

            // Advance node
            if (ni == -1) {
                parent = node;
                prev = null;
                ni = 0;
                node = (node._children == null) ? null : node._children[c % _width];
            }

            // Loop through a node chain at the same level
            while (node != null) {
                // If it is a matching node, goto next char
                if (node._char[ni] == c || _ignoreCase && node._ochar[ni] == c) {
                    prev = null;
                    ni++;
                    if (ni == node._char.length)
                        ni = -1;
                    continue charLoop;
                }

                // no char match
                // if the first char,
                if (ni == 0) {
                    // look along the chain for a char match
                    prev = node;
                    node = node._next;
                } else {
                    // Split the current node!
                    node.split(this, ni);
                    i--;
                    ni = -1;
                    continue charLoop;
                }
            }

            // We have run out of nodes, so as this is a put, make one
            node = new Node(_ignoreCase, key, i);

            if (prev != null) // add to end of chain
                prev._next = node;
            else if (parent != null) // add new child
            {
                if (parent._children == null)
                    parent._children = new Node[_width];
                parent._children[c % _width] = node;
                int oi = node._ochar[0] % _width;
                if (node._ochar != null && node._char[0] % _width != oi) {
                    if (parent._children[oi] == null)
                        parent._children[oi] = node;
                    else {
                        Node n = parent._children[oi];
                        while (n._next != null)
                            n = n._next;
                        n._next = node;
                    }
                }
            } else // this is the root.
                _root = node;
            break;
        }

        // Do we have a node
        if (node != null) {
            // Split it if we are in the middle
            if (ni > 0)
                node.split(this, ni);

            Object old = node._value;
            node._key = key;
            node._value = value;
            _entrySet.add(node);
            return old;
        }
        return null;
    }

    /* ------------------------------------------------------------ */
    public Object get(Object key) {
        if (key == null)
            return _nullValue;
        if (key instanceof String)
            return get((String) key);
        return get(key.toString());
    }

    /* ------------------------------------------------------------ */
    public Object get(String key) {
        if (key == null)
            return _nullValue;

        Map.Entry entry = getEntry(key, 0, key.length());
        if (entry == null)
            return null;
        return entry.getValue();
    }

    /* ------------------------------------------------------------ */
    /** Get a map entry by substring key.
     * @param key String containing the key
     * @param offset Offset of the key within the String.
     * @param length The length of the key 
     * @return The Map.Entry for the key or null if the key is not in
     * the map.
     */
    public Map.Entry getEntry(String key, int offset, int length) {
        if (key == null)
            return _nullEntry;

        Node node = _root;
        int ni = -1;

        // look for best match
        charLoop: for (int i = 0; i < length; i++) {
            char c = key.charAt(offset + i);

            // Advance node
            if (ni == -1) {
                ni = 0;
                node = (node._children == null) ? null : node._children[c % _width];
            }

            // Look through the node chain
            while (node != null) {
                // If it is a matching node, goto next char
                if (node._char[ni] == c || _ignoreCase && node._ochar[ni] == c) {
                    ni++;
                    if (ni == node._char.length)
                        ni = -1;
                    continue charLoop;
                }

                // No char match, so if mid node then no match at all.
                if (ni > 0)
                    return null;

                // try next in chain
                node = node._next;
            }
            return null;
        }

        if (ni > 0)
            return null;
        if (node != null && node._key == null)
            return null;
        return node;
    }

    /* ------------------------------------------------------------ */
    /** Get a map entry by char array key.
     * @param key char array containing the key
     * @param offset Offset of the key within the array.
     * @param length The length of the key 
     * @return The Map.Entry for the key or null if the key is not in
     * the map.
     */
    public Map.Entry getEntry(char[] key, int offset, int length) {
        if (key == null)
            return _nullEntry;

        Node node = _root;
        int ni = -1;

        // look for best match
        charLoop: for (int i = 0; i < length; i++) {
            char c = key[offset + i];

            // Advance node
            if (ni == -1) {
                ni = 0;
                node = (node._children == null) ? null : node._children[c % _width];
            }

            // While we have a node to try
            while (node != null) {
                // If it is a matching node, goto next char
                if (node._char[ni] == c || _ignoreCase && node._ochar[ni] == c) {
                    ni++;
                    if (ni == node._char.length)
                        ni = -1;
                    continue charLoop;
                }

                // No char match, so if mid node then no match at all.
                if (ni > 0)
                    return null;

                // try next in chain
                node = node._next;
            }
            return null;
        }

        if (ni > 0)
            return null;
        if (node != null && node._key == null)
            return null;
        return node;
    }

    /* ------------------------------------------------------------ */
    /** Get a map entry by byte array key, using as much of the passed key as needed for a match.
     * A simple 8859-1 byte to char mapping is assumed.
     * @param key char array containing the key
     * @param offset Offset of the key within the array.
     * @param maxLength The length of the key 
     * @return The Map.Entry for the key or null if the key is not in
     * the map.
     */
    public Map.Entry getBestEntry(byte[] key, int offset, int maxLength) {
        if (key == null)
            return _nullEntry;

        Node node = _root;
        int ni = -1;

        // look for best match
        charLoop: for (int i = 0; i < maxLength; i++) {
            char c = (char) key[offset + i];

            // Advance node
            if (ni == -1) {
                ni = 0;

                Node child = (node._children == null) ? null : node._children[c % _width];

                if (child == null && i > 0)
                    return node; // This is the best match
                node = child;
            }

            // While we have a node to try
            while (node != null) {
                // If it is a matching node, goto next char
                if (node._char[ni] == c || _ignoreCase && node._ochar[ni] == c) {
                    ni++;
                    if (ni == node._char.length)
                        ni = -1;
                    continue charLoop;
                }

                // No char match, so if mid node then no match at all.
                if (ni > 0)
                    return null;

                // try next in chain
                node = node._next;
            }
            return null;
        }

        if (ni > 0)
            return null;
        if (node != null && node._key == null)
            return null;
        return node;
    }

    /* ------------------------------------------------------------ */
    public Object remove(Object key) {
        if (key == null)
            return remove(null);
        return remove(key.toString());
    }

    /* ------------------------------------------------------------ */
    public Object remove(String key) {
        if (key == null) {
            Object oldValue = _nullValue;
            if (_nullEntry != null) {
                _entrySet.remove(_nullEntry);
                _nullEntry = null;
                _nullValue = null;
            }
            return oldValue;
        }

        Node node = _root;
        int ni = -1;

        // look for best match
        charLoop: for (int i = 0; i < key.length(); i++) {
            char c = key.charAt(i);

            // Advance node
            if (ni == -1) {
                ni = 0;
                node = (node._children == null) ? null : node._children[c % _width];
            }

            // While we have a node to try
            while (node != null) {
                // If it is a matching node, goto next char
                if (node._char[ni] == c || _ignoreCase && node._ochar[ni] == c) {
                    ni++;
                    if (ni == node._char.length)
                        ni = -1;
                    continue charLoop;
                }

                // No char match, so if mid node then no match at all.
                if (ni > 0)
                    return null;

                // try next in chain
                node = node._next;
            }
            return null;
        }

        if (ni > 0)
            return null;
        if (node != null && node._key == null)
            return null;

        Object old = node._value;
        _entrySet.remove(node);
        node._value = null;
        node._key = null;

        return old;
    }

    /* ------------------------------------------------------------ */
    public Set entrySet() {
        return _umEntrySet;
    }

    /* ------------------------------------------------------------ */
    public int size() {
        return _entrySet.size();
    }

    /* ------------------------------------------------------------ */
    public boolean isEmpty() {
        return _entrySet.isEmpty();
    }

    /* ------------------------------------------------------------ */
    public boolean containsKey(Object key) {
        if (key == null)
            return _nullEntry != null;
        return getEntry(key.toString(), 0, key == null ? 0 : key.toString().length()) != null;
    }

    /* ------------------------------------------------------------ */
    public void clear() {
        _root = new Node();
        _nullEntry = null;
        _nullValue = null;
        _entrySet.clear();
    }

    /* ------------------------------------------------------------ */
    /* ------------------------------------------------------------ */
    /* ------------------------------------------------------------ */
    private static class Node implements Map.Entry {
        char[] _char;
        char[] _ochar;
        Node _next;
        Node[] _children;
        String _key;
        Object _value;

        Node() {
        }

        Node(boolean ignoreCase, String s, int offset) {
            int l = s.length() - offset;
            _char = new char[l];
            _ochar = new char[l];
            for (int i = 0; i < l; i++) {
                char c = s.charAt(offset + i);
                _char[i] = c;
                if (ignoreCase) {
                    char o = c;
                    if (Character.isUpperCase(c))
                        o = Character.toLowerCase(c);
                    else if (Character.isLowerCase(c))
                        o = Character.toUpperCase(c);
                    _ochar[i] = o;
                }
            }
        }

        Node split(StringMap map, int offset) {
            Node split = new Node();
            int sl = _char.length - offset;

            char[] tmp = this._char;
            this._char = new char[offset];
            split._char = new char[sl];
            System.arraycopy(tmp, 0, this._char, 0, offset);
            System.arraycopy(tmp, offset, split._char, 0, sl);

            if (this._ochar != null) {
                tmp = this._ochar;
                this._ochar = new char[offset];
                split._ochar = new char[sl];
                System.arraycopy(tmp, 0, this._ochar, 0, offset);
                System.arraycopy(tmp, offset, split._ochar, 0, sl);
            }

            split._key = this._key;
            split._value = this._value;
            this._key = null;
            this._value = null;
            if (map._entrySet.remove(this))
                map._entrySet.add(split);

            split._children = this._children;
            this._children = new Node[map._width];
            this._children[split._char[0] % map._width] = split;
            if (split._ochar != null && this._children[split._ochar[0] % map._width] != split)
                this._children[split._ochar[0] % map._width] = split;

            return split;
        }

        public Object getKey() {
            return _key;
        }

        public Object getValue() {
            return _value;
        }

        public Object setValue(Object o) {
            Object old = _value;
            _value = o;
            return old;
        }

        public String toString() {
            StringBuffer buf = new StringBuffer();
            synchronized (buf) {
                toString(buf);
            }
            return buf.toString();
        }

        private void toString(StringBuffer buf) {
            buf.append("{[");
            if (_char == null)
                buf.append('-');
            else
                for (int i = 0; i < _char.length; i++)
                    buf.append(_char[i]);
            buf.append(':');
            buf.append(_key);
            buf.append('=');
            buf.append(_value);
            buf.append(']');
            if (_children != null) {
                for (int i = 0; i < _children.length; i++) {
                    buf.append('|');
                    if (_children[i] != null)
                        _children[i].toString(buf);
                    else
                        buf.append("-");
                }
            }
            buf.append('}');
            if (_next != null) {
                buf.append(",\n");
                _next.toString(buf);
            }
        }
    }

    /* ------------------------------------------------------------ */
    /* ------------------------------------------------------------ */
    private class NullEntry implements Map.Entry {
        public Object getKey() {
            return null;
        }

        public Object getValue() {
            return _nullValue;
        }

        public Object setValue(Object o) {
            Object old = _nullValue;
            _nullValue = o;
            return old;
        }

        public String toString() {
            return "[:null=" + _nullValue + "]";
        }
    }

    /* ------------------------------------------------------------ */
    public void writeExternal(java.io.ObjectOutput out) throws java.io.IOException {
        HashMap map = new HashMap(this);
        out.writeBoolean(_ignoreCase);
        out.writeObject(map);
    }

    /* ------------------------------------------------------------ */
    public void readExternal(java.io.ObjectInput in) throws java.io.IOException, ClassNotFoundException {
        boolean ic = in.readBoolean();
        HashMap map = (HashMap) in.readObject();
        setIgnoreCase(ic);
        this.putAll(map);
    }
}