Java tutorial
// 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); } }