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