/* -*- Mode: java; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 4 -*-
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
package org.mozilla.javascript;
import java.io.Serializable;
import java.math.BigInteger;
import java.util.HashMap;
import java.util.Iterator;
import java.util.NoSuchElementException;
/**
* This generic hash table class is used by Set and Map. It uses a standard HashMap for storing keys
* and values so that we can handle lots of hash collisions if necessary, and a doubly-linked list
* to support the iterator capability.
*
* This second one is important because JavaScript handling of the iterator is completely
* different from the way that Java does it. In Java an attempt to modify a collection on a HashMap
* or LinkedHashMap while iterating through it (except by using the "remove" method on the Iterator
* object itself) results in a ConcurrentModificationException. JavaScript Maps and Sets explicitly
* allow the collection to be modified, or even cleared completely, while iterators exist, and even
* lets an iterator keep on iterating on a collection that was empty when it was created..
*/
public class Hashtable implements Serializable, Iterable {
private static final long serialVersionUID = -7151554912419543747L;
private final HashMap map = new HashMap<>();
private Entry first = null;
private Entry last = null;
/**
* One entry in the hash table. Override equals and hashcode because this is another area in
* which JavaScript and Java differ. This entry also becomes a node in the linked list.
*/
public static final class Entry implements Serializable {
private static final long serialVersionUID = 4086572107122965503L;
protected Object key;
protected Object value;
protected boolean deleted;
protected Entry next;
protected Entry prev;
private final int hashCode;
Entry() {
hashCode = 0;
}
Entry(Object k, Object value) {
if (k instanceof Number) {
if (k instanceof Double || k instanceof BigInteger) {
// BigInteger needs to retain its own type, due to
// "If Type(x) is different from Type(y), return false." in
// ecma262/multipage/abstract-operations.html#sec-samevaluezero
key = k;
} else {
// Hash comparison won't work if we don't do this
key = Double.valueOf(((Number) k).doubleValue());
}
} else if (k instanceof ConsString) {
key = k.toString();
} else {
key = k;
}
if (key == null) {
hashCode = 0;
} else if (k.equals(ScriptRuntime.negativeZeroObj)) {
hashCode = 0;
} else {
hashCode = key.hashCode();
}
this.value = value;
}
public Object key() {
return key;
}
public Object value() {
return value;
}
/** Zero out key and value and return old value. */
void clear() {
key = Undefined.instance;
value = Undefined.instance;
deleted = true;
}
@Override
public int hashCode() {
return hashCode;
}
@Override
public boolean equals(Object o) {
if (o == null) {
return false;
}
try {
return ScriptRuntime.sameZero(key, ((Entry) o).key);
} catch (ClassCastException cce) {
return false;
}
}
}
private static Entry makeDummy() {
final Entry d = new Entry();
d.clear();
return d;
}
public int size() {
return map.size();
}
public void put(Object key, Object value) {
final Entry nv = new Entry(key, value);
final Entry ev = map.putIfAbsent(nv, nv);
if (ev == null) {
// New value -- insert to end of doubly-linked list
if (first == null) {
first = last = nv;
} else {
last.next = nv;
nv.prev = last;
last = nv;
}
} else {
// Update the existing value and keep it in the same place in the list
ev.value = value;
}
}
/**
* @deprecated use getEntry(Object key) instead because this returns null if the entry was not
* found or the value of the entry is null
*/
public Object get(Object key) {
final Entry e = new Entry(key, null);
final Entry v = map.get(e);
if (v == null) {
return null;
}
return v.value;
}
public Entry getEntry(Object key) {
final Entry e = new Entry(key, null);
return map.get(e);
}
public boolean has(Object key) {
final Entry e = new Entry(key, null);
return map.containsKey(e);
}
/**
* @deprecated use deleteEntry(Object key) instead because this returns null if the entry was
* not found or the value of the entry is null
*/
public Object delete(Object key) {
final Entry e = new Entry(key, null);
final Entry v = map.remove(e);
if (v == null) {
return null;
}
// To keep existing iterators moving forward as specified in EC262,
// we will remove the "prev" pointers from the list but leave the "next"
// pointers intact. Once we do that, then the only things pointing to
// the deleted nodes are existing iterators. Once those are gone, then
// these objects will be GCed.
// This way, new iterators will not "see" the deleted elements, and
// existing iterators will continue from wherever they left off to
// continue iterating in insertion order.
if (v == first) {
if (v == last) {
// Removing the only element. Leave it as a dummy or existing iterators
// will never stop.
v.clear();
v.prev = null;
} else {
first = v.next;
first.prev = null;
if (first.next != null) {
first.next.prev = first;
}
}
} else {
final Entry prev = v.prev;
prev.next = v.next;
v.prev = null;
if (v.next != null) {
v.next.prev = prev;
} else {
assert (v == last);
last = prev;
}
}
// Still clear the node in case it is in the chain of some iterator
final Object ret = v.value;
v.clear();
return ret;
}
public boolean deleteEntry(Object key) {
final Entry e = new Entry(key, null);
final Entry v = map.remove(e);
if (v == null) {
return false;
}
// To keep existing iterators moving forward as specified in EC262,
// we will remove the "prev" pointers from the list but leave the "next"
// pointers intact. Once we do that, then the only things pointing to
// the deleted nodes are existing iterators. Once those are gone, then
// these objects will be GCed.
// This way, new iterators will not "see" the deleted elements, and
// existing iterators will continue from wherever they left off to
// continue iterating in insertion order.
if (v == first) {
if (v == last) {
// Removing the only element. Leave it as a dummy or existing iterators
// will never stop.
v.clear();
v.prev = null;
} else {
first = v.next;
first.prev = null;
if (first.next != null) {
first.next.prev = first;
}
}
} else {
final Entry prev = v.prev;
prev.next = v.next;
v.prev = null;
if (v.next != null) {
v.next.prev = prev;
} else {
assert (v == last);
last = prev;
}
}
// Still clear the node in case it is in the chain of some iterator
v.clear();
return true;
}
public void clear() {
// Zero out all the entries so that existing iterators will skip them all
Iterator it = iterator();
it.forEachRemaining(Entry::clear);
// Replace the existing list with a dummy, and make it the last node
// of the current list. If new nodes are added now, existing iterators
// will drive forward right into the new list. If they are not, then
// nothing is referencing the old list and it'll get GCed.
if (first != null) {
Entry dummy = makeDummy();
last.next = dummy;
first = last = dummy;
}
// Now we can clear the actual hashtable!
map.clear();
}
public Iterator iterator() {
return new Iter(first);
}
// The iterator for this class works directly on the linked list so that it implements
// the specified iteration behavior, which is very different from Java.
private static final class Iter implements Iterator {
private Entry pos;
Iter(Entry start) {
// Keep the logic simpler by having a dummy at the start
Entry dummy = makeDummy();
dummy.next = start;
this.pos = dummy;
}
private void skipDeleted() {
// Skip forward past deleted elements, which could appear due to
// "delete" or a "clear" operation after this iterator was created.
// End up just before the next non-deleted node.
while ((pos.next != null) && pos.next.deleted) {
pos = pos.next;
}
}
@Override
public boolean hasNext() {
skipDeleted();
return ((pos != null) && (pos.next != null));
}
@Override
public Entry next() {
skipDeleted();
if ((pos == null) || (pos.next == null)) {
throw new NoSuchElementException();
}
final Entry e = pos.next;
pos = pos.next;
return e;
}
}
}