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