blob: 5a8a6139c0d9dcb8d66811583e36bff8ca9a4816 [file] [log] [blame]
/*
* $Id: AbstractTableMap.java 38 2012-01-04 22:44:15Z andre@naef.com $
* See LICENSE.txt for license terms.
*/
package com.naef.jnlua.util;
import java.util.AbstractMap;
import java.util.AbstractSet;
import java.util.Iterator;
import java.util.Map;
import java.util.NoSuchElementException;
import java.util.Set;
import com.naef.jnlua.LuaState;
import com.naef.jnlua.LuaValueProxy;
/**
* Abstract map implementation backed by a Lua table.
*/
public abstract class AbstractTableMap<K> extends AbstractMap<K, Object>
implements LuaValueProxy {
// -- State
private Set<Map.Entry<K, Object>> entrySet;
// -- Construction
/**
* Creates a new instance.
*/
public AbstractTableMap() {
}
// -- Map methods
@Override
public Set<Map.Entry<K, Object>> entrySet() {
if (entrySet == null) {
entrySet = new EntrySet();
}
return entrySet;
}
@Override
public boolean isEmpty() {
return entrySet().isEmpty();
}
@Override
public boolean containsKey(Object key) {
checkKey(key);
LuaState luaState = getLuaState();
synchronized (luaState) {
pushValue();
luaState.pushJavaObject(key);
luaState.getTable(-2);
try {
return !luaState.isNil(-1);
} finally {
luaState.pop(2);
}
}
}
@Override
public Object get(Object key) {
checkKey(key);
LuaState luaState = getLuaState();
synchronized (luaState) {
pushValue();
luaState.pushJavaObject(key);
luaState.getTable(-2);
try {
return luaState.toJavaObject(-1, Object.class);
} finally {
luaState.pop(2);
}
}
}
@Override
public Object put(K key, Object value) {
checkKey(key);
LuaState luaState = getLuaState();
synchronized (luaState) {
Object oldValue = get(key);
pushValue();
luaState.pushJavaObject(key);
luaState.pushJavaObject(value);
luaState.setTable(-3);
luaState.pop(1);
return oldValue;
}
}
@Override
public Object remove(Object key) {
checkKey(key);
LuaState luaState = getLuaState();
synchronized (luaState) {
Object oldValue = get(key);
pushValue();
luaState.pushJavaObject(key);
luaState.pushNil();
luaState.setTable(-3);
luaState.pop(1);
return oldValue;
}
}
// -- Protected methods
/**
* Checks a key for validity. If the key is not valid, the method throws an
* appropriate runtime exception. The method is invoked for all input keys.
*
* <p>
* This implementation checks that the key is not <code>null</code>. Lua
* does not allow <code>nil</code> as a table key. Subclasses may implement
* more restrictive checks.
* </p>
*
* @param key
* the key
* @throws NullPointerException
* if the key is <code>null</code>
*/
protected void checkKey(Object key) {
if (key == null) {
throw new NullPointerException("key must not be null");
}
}
/**
* Indicates if this table map filters keys from the Lua table. If the
* method returns <code>true</code>, the table map invokes
* {@link #acceptKey(int)} on each key retrieved from the underlying table
* to determine whether the key is accepted or rejected.
*
* <p>
* This implementation returns <code>false</code>. Subclasses may override
* the method alongside {@link #acceptKey(int)} to implement key filtering.
* </p>
*
* @return whether this table map filters keys from the Lua table
*/
protected boolean filterKeys() {
return false;
}
/**
* Accepts or rejects a key from the Lua table. Only table keys that are
* accepted are processed. The method allows subclasses to filter the Lua
* table. The method is called only if {@link #filterKeys()} returns
* <code>true</code>.
*
* <p>
* This implementation returns <code>true</code> regardless of the input,
* thus accepting all keys. Subclasses may override the method alongside
* {@link #filterKeys()} to implement key filtering.
* </p>
*
* @param index
* the stack index containing the candidate key
* @return whether the key is accepted
*/
protected boolean acceptKey(int index) {
return true;
}
/**
* Converts the key at the specified stack index to a Java object. If this
* table maps performs key filtering, the method is invoked only for keys it
* has accepted.
*
* @param index
* the stack index containing the key
* @return the Java object representing the key
* @see #filterKeys()
* @see #acceptKey(int)
*/
protected abstract K convertKey(int index);
// -- Nested types
/**
* Lua table entry set.
*/
private class EntrySet extends AbstractSet<Map.Entry<K, Object>> {
// -- Set methods
@Override
public Iterator<Map.Entry<K, Object>> iterator() {
return new EntryIterator();
}
@Override
public boolean isEmpty() {
LuaState luaState = getLuaState();
synchronized (luaState) {
pushValue();
luaState.pushNil();
while (luaState.next(-2)) {
if (!filterKeys() || acceptKey(-2)) {
luaState.pop(3);
return false;
}
}
luaState.pop(1);
return true;
}
}
@Override
public int size() {
LuaState luaState = getLuaState();
synchronized (luaState) {
int count = 0;
pushValue();
if (filterKeys()) {
luaState.pushNil();
while (luaState.next(-2)) {
if (acceptKey(-2)) {
count++;
}
luaState.pop(1);
}
} else {
count = luaState.tableSize(-1);
}
luaState.pop(1);
return count;
}
}
@Override
public boolean contains(Object object) {
checkKey(object);
if (!(object instanceof AbstractTableMap<?>.Entry)) {
return false;
}
@SuppressWarnings("unchecked")
Entry luaTableEntry = (Entry) object;
if (luaTableEntry.getLuaState() != getLuaState()) {
return false;
}
return containsKey(luaTableEntry.key);
}
@Override
public boolean remove(Object object) {
if (!(object instanceof AbstractTableMap<?>.Entry)) {
return false;
}
@SuppressWarnings("unchecked")
Entry luaTableEntry = (Entry) object;
if (luaTableEntry.getLuaState() != getLuaState()) {
return false;
}
LuaState luaState = getLuaState();
synchronized (luaState) {
pushValue();
luaState.pushJavaObject(object);
luaState.getTable(-2);
boolean contains = !luaState.isNil(-1);
luaState.pop(1);
if (contains) {
luaState.pushJavaObject(object);
luaState.pushNil();
luaState.setTable(-3);
}
luaState.pop(1);
return contains;
}
}
}
/**
* Lua table iterator.
*/
private class EntryIterator implements Iterator<Map.Entry<K, Object>> {
// -- State
private K key;
// -- Iterator methods
@Override
public boolean hasNext() {
LuaState luaState = getLuaState();
synchronized (luaState) {
pushValue();
luaState.pushJavaObject(key);
while (luaState.next(-2)) {
if (!filterKeys() || acceptKey(-2)) {
luaState.pop(3);
return true;
}
}
luaState.pop(1);
return false;
}
}
@Override
public Map.Entry<K, Object> next() {
LuaState luaState = getLuaState();
synchronized (luaState) {
pushValue();
luaState.pushJavaObject(key);
while (luaState.next(-2)) {
if (!filterKeys() || acceptKey(-2)) {
key = convertKey(-2);
luaState.pop(3);
return new Entry(key);
}
}
luaState.pop(1);
throw new NoSuchElementException();
}
}
@Override
public void remove() {
LuaState luaState = getLuaState();
synchronized (luaState) {
pushValue();
luaState.pushJavaObject(key);
luaState.pushNil();
luaState.setTable(-3);
luaState.pop(1);
}
}
}
/**
* Bindings entry.
*/
private class Entry implements Map.Entry<K, Object> {
// -- State
private K key;
// -- Construction
/**
* Creates a new instance.
*/
public Entry(K key) {
this.key = key;
}
// -- Map.Entry methods
@Override
public K getKey() {
return key;
}
@Override
public Object getValue() {
return get(key);
}
@Override
public Object setValue(Object value) {
return put(key, value);
}
// -- Object methods
@Override
public boolean equals(Object obj) {
if (!(obj instanceof AbstractTableMap<?>.Entry)) {
return false;
}
@SuppressWarnings("unchecked")
Entry other = (Entry) obj;
return getLuaState() == other.getLuaState()
&& key.equals(other.key);
}
@Override
public int hashCode() {
return getLuaState().hashCode() * 65599 + key.hashCode();
}
@Override
public String toString() {
return key.toString();
}
// -- Private methods
/**
* Returns the Lua script engine.
*/
private LuaState getLuaState() {
return AbstractTableMap.this.getLuaState();
}
}
}