diff --git a/rembulan-runtime/pom.xml b/rembulan-runtime/pom.xml index 44f6086ac..6464db3df 100644 --- a/rembulan-runtime/pom.xml +++ b/rembulan-runtime/pom.xml @@ -38,8 +38,18 @@ scm:git:git://github.com/mjanicek/rembulan.git https://github.com/mjanicek/rembulan/tree/master + + + src/main/java + src/main/test + + + junit + junit + 4.12 + diff --git a/rembulan-runtime/src/main/java/net/sandius/rembulan/impl/ImmutableTable.java b/rembulan-runtime/src/main/java/net/sandius/rembulan/impl/ImmutableTable.java index 4e43cfce7..b08527caf 100644 --- a/rembulan-runtime/src/main/java/net/sandius/rembulan/impl/ImmutableTable.java +++ b/rembulan-runtime/src/main/java/net/sandius/rembulan/impl/ImmutableTable.java @@ -1,337 +1,359 @@ /* * Copyright 2016 Miroslav Janíček * - * 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 + * 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 + * 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. + * 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. */ package net.sandius.rembulan.impl; -import net.sandius.rembulan.Conversions; -import net.sandius.rembulan.Table; -import net.sandius.rembulan.TableFactory; -import net.sandius.rembulan.util.TraversableHashMap; - import java.util.Collections; import java.util.HashMap; import java.util.Map; import java.util.Objects; +import net.sandius.rembulan.Conversions; +import net.sandius.rembulan.Table; +import net.sandius.rembulan.TableFactory; +import net.sandius.rembulan.util.TraversableHashMap; + /** * An immutable table. * - *

The contents of this table may be queried, but not changed: the methods + *

+ * The contents of this table may be queried, but not changed: the methods * {@link #rawset(Object, Object)}, {@link #rawset(long, Object)} and {@link #setMetatable(Table)} - * will throw an {@link UnsupportedOperationException}.

+ * will throw an {@link UnsupportedOperationException}. + *

* - *

The table has no metatable.

+ *

+ * The table has no metatable. + *

* - *

To instantiate a new {@code ImmutableTable}, use one of the static constructor methods - * (e.g., {@link #of(Iterable)}), or a {@link ImmutableTable.Builder} as follows:

+ *

+ * To instantiate a new {@code ImmutableTable}, use one of the static constructor methods (e.g., + * {@link #of(Iterable)}), or a {@link ImmutableTable.Builder} as follows: + *

* *
- *     ImmutableTable t = new ImmutableTable.Builder()
- *         .add("key1", "value1")
- *         .add("key2", "value2")
- *         .build();
+ * ImmutableTable t =
+ *     new ImmutableTable.Builder().add("key1", "value1").add("key2", "value2").build();
  * 
* - *

A word of caution: this class violates the expectation that all Lua tables are - * mutable, and should therefore be used with care. In order to create a mutable copy of this - * table, use {@link #newCopy(TableFactory)}.

+ *

+ * A word of caution: this class violates the expectation that all Lua tables are mutable, + * and should therefore be used with care. In order to create a mutable copy of this table, use + * {@link #newCopy(TableFactory)}. + *

*/ public class ImmutableTable extends Table { - private final Map entries; - private final Object initialKey; // null iff the table is empty - - static class Entry { - - private final Object value; - private final Object nextKey; // may be null - - private Entry(Object value, Object nextKey) { - this.value = Objects.requireNonNull(value); - this.nextKey = nextKey; - } - - } - - ImmutableTable(Map entries, Object initialKey) { - this.entries = Objects.requireNonNull(entries); - this.initialKey = initialKey; - } - - /** - * Returns an {@code ImmutableTable} based on the contents of the sequence of - * map entries {@code entries}. - * - *

For every {@code key}-{@code value} pair in {@code entries}, the behaviour of this - * method is similar to that of {@link Table#rawset(Object, Object)}:

- * - * - *

Keys may occur multiple times in {@code entries} — only the last occurrence - * counts.

- * - * @param entries the map entries, must not be {@code null} - * @return an immutable table based on the contents of {@code entries} - * - * @throws NullPointerException if {@code entries} is {@code null} - * @throws IllegalArgumentException if {@code entries} contains an entry with - * a {@code null} or NaN key - */ - public static ImmutableTable of(Iterable> entries) { - Builder builder = new Builder(); - for (Map.Entry entry : entries) { - builder.add(entry.getKey(), entry.getValue()); - } - return builder.build(); - } - - /** - * Returns an {@code ImmutableTable} based on the contents of the map {@code map}. - * - *

For every {@code key}-{@code value} pair in {@code map}, the behaviour of this method - * is similar to that of {@link Table#rawset(Object, Object)}:

- *
    - *
  • when {@code value} is nil (i.e., {@code null}), then {@code key} - * will not have any value associated with it in the resulting table;
  • - *
  • if {@code key} is nil or NaN, a {@link IllegalArgumentException} - * is thrown;
  • - *
  • if {@code key} is a number that has an integer value, it is converted to that integer - * value.
  • - *
- * - * @param map the map used to source the contents of the table, must not be {@code null} - * @return an immutable table based on the contents of {@code map} - * - * @throws NullPointerException if {@code entries} is {@code null} - * @throws IllegalArgumentException if {@code map} contains a {@code null} or NaN key - */ - public static ImmutableTable of(Map map) { - return of(map.entrySet()); - } - - /** - * Returns a new table constructed using the supplied {@code tableFactory}, and copies - * the contents of this table to it. - * - * @param tableFactory the table factory to use, must not be {@code null} - * @return a mutable copy of this table - */ - public Table newCopy(TableFactory tableFactory) { - Table t = tableFactory.newTable(); - for (Object key : entries.keySet()) { - Entry e = entries.get(key); - t.rawset(key, e.value); - } - return t; - } - - @Override - public boolean equals(Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; - ImmutableTable that = (ImmutableTable) o; - return this.entries.equals(that.entries) - && this.initialKey.equals(that.initialKey); - } - - @Override - public int hashCode() { - int result = entries.hashCode(); - result = 31 * result + initialKey.hashCode(); - return result; - } - - @Override - public Object rawget(Object key) { - key = Conversions.normaliseKey(key); - Entry e = entries.get(key); - return e != null ? e.value : null; - } - - /** - * Throws an {@link UnsupportedOperationException}, since this table is immutable. - * - * @param key ignored - * @param value ignored - * - * @throws UnsupportedOperationException every time this method is called - */ - @Override - public void rawset(Object key, Object value) { - throw new UnsupportedOperationException("table is immutable"); - } - - /** - * Throws an {@link UnsupportedOperationException}, since this table is immutable. - * - * @param idx ignored - * @param value ignored - * - * @throws UnsupportedOperationException every time this method is called - */ - @Override - public void rawset(long idx, Object value) { - throw new UnsupportedOperationException("table is immutable"); - } - - @Override - public Table getMetatable() { - return null; - } - - /** - * Throws an {@link UnsupportedOperationException}, since this table is immutable. - * - * @param mt ignored - * @return nothing (always throws an exception) - * - * @throws UnsupportedOperationException every time this method is called - */ - @Override - public Table setMetatable(Table mt) { - throw new UnsupportedOperationException("table is immutable"); - } - - @Override - public Object initialKey() { - return initialKey; - } - - @Override - public Object successorKeyOf(Object key) { - key = Conversions.normaliseKey(key); - try { - Entry e = entries.get(key); - return e.nextKey; - } - catch (NullPointerException ex) { - throw new IllegalArgumentException("invalid key to 'next'", ex); - } - } - - @Override - protected void setMode(boolean weakKeys, boolean weakValues) { - // no-op - } - - /** - * Builder class for constructing instances of {@link ImmutableTable}. - */ - public static class Builder { - - private final TraversableHashMap entries; - - private static void checkKey(Object key) { - if (key == null || (key instanceof Double && Double.isNaN(((Double) key).doubleValue()))) { - throw new IllegalArgumentException("invalid table key: " + Conversions.toHumanReadableString(key)); - } - } - - private Builder(TraversableHashMap entries) { - this.entries = Objects.requireNonNull(entries); - } - - /** - * Constructs a new empty builder. - */ - public Builder() { - this(new TraversableHashMap<>()); - } - - private static TraversableHashMap mapCopy(TraversableHashMap map) { - TraversableHashMap result = new TraversableHashMap<>(); - result.putAll(map); - return result; - } - - /** - * Constructs a copy of the given builder (a copy constructor). - * - * @param builder the original builder, must not be {@code null} - * - * @throws NullPointerException if {@code builder} is {@code null} - */ - public Builder(Builder builder) { - this(mapCopy(builder.entries)); - } - - /** - * Sets the value associated with the key {@code key} to {@code value}. - * - *

The behaviour of this method is similar to that of - * {@link Table#rawset(Object, Object)}:

- *
    - *
  • when {@code value} is nil (i.e., {@code null}), the key {@code key} - * will not have any value associated with it after this method returns;
  • - *
  • nil and NaN keys are rejected by throwing - * a {@link IllegalArgumentException};
  • - *
  • numeric keys with an integer value are converted to that integer value.
  • - *
- * - *

The method returns {@code this}, allowing calls to this method to be chained.

- * - * @param key the key, must not be {@code null} or NaN - * @param value the value, may be {@code null} - * @return this builder - * - * @throws IllegalArgumentException when {@code key} is {@code null} or a NaN - */ - public Builder add(Object key, Object value) { - key = Conversions.normaliseKey(key); - checkKey(key); - - if (value != null) { - entries.put(key, value); - } - else { - entries.remove(key); - } - - return this; - } - - /** - * Clears the builder. - */ - public void clear() { - entries.clear(); - } - - /** - * Constructs and returns a new immutable table based on the contents of this - * builder. - * - * @return a new immutable table - */ - public ImmutableTable build() { - Map tableEntries = new HashMap<>(); - - for (Map.Entry e : entries.entrySet()) { - Object k = e.getKey(); - tableEntries.put(e.getKey(), new Entry(e.getValue(), entries.getSuccessorOf(k))); - } - return new ImmutableTable(Collections.unmodifiableMap(tableEntries), entries.getFirstKey()); - } - - } + private final Map entries; + private final Object initialKey; // null iff the table is empty + + static class Entry { + + private final Object value; + private final Object nextKey; // may be null + + private Entry(Object value, Object nextKey) { + this.value = Objects.requireNonNull(value); + this.nextKey = nextKey; + } + + } + + ImmutableTable(Map entries, Object initialKey) { + this.entries = Objects.requireNonNull(entries); + this.initialKey = initialKey; + } + + /** + * Returns an {@code ImmutableTable} based on the contents of the sequence of map entries + * {@code entries}. + * + *

+ * For every {@code key}-{@code value} pair in {@code entries}, the behaviour of this method is + * similar to that of {@link Table#rawset(Object, Object)}: + *

+ *
    + *
  • when {@code value} is nil (i.e., {@code null}), then {@code key} will not have any + * value associated with it in the resulting table;
  • + *
  • if {@code key} is nil or NaN, a {@link IllegalArgumentException} is thrown; + *
  • + *
  • if {@code key} is a number that has an integer value, it is converted to that integer + * value.
  • + *
+ * + *

+ * Keys may occur multiple times in {@code entries} — only the last occurrence counts. + *

+ * + * @param entries the map entries, must not be {@code null} + * @return an immutable table based on the contents of {@code entries} + * + * @throws NullPointerException if {@code entries} is {@code null} + * @throws IllegalArgumentException if {@code entries} contains an entry with a {@code null} or + * NaN key + */ + public static ImmutableTable of(Iterable> entries) { + Builder builder = new Builder(); + for (Map.Entry entry : entries) { + builder.add(entry.getKey(), entry.getValue()); + } + return builder.build(); + } + + /** + * Returns an {@code ImmutableTable} based on the contents of the map {@code map}. + * + *

+ * For every {@code key}-{@code value} pair in {@code map}, the behaviour of this method is + * similar to that of {@link Table#rawset(Object, Object)}: + *

+ *
    + *
  • when {@code value} is nil (i.e., {@code null}), then {@code key} will not have any + * value associated with it in the resulting table;
  • + *
  • if {@code key} is nil or NaN, a {@link IllegalArgumentException} is thrown; + *
  • + *
  • if {@code key} is a number that has an integer value, it is converted to that integer + * value.
  • + *
+ * + * @param map the map used to source the contents of the table, must not be {@code null} + * @return an immutable table based on the contents of {@code map} + * + * @throws NullPointerException if {@code entries} is {@code null} + * @throws IllegalArgumentException if {@code map} contains a {@code null} or NaN key + */ + public static ImmutableTable of(Map map) { + return of(map.entrySet()); + } + + /** + * Returns a new table constructed using the supplied {@code tableFactory}, and copies the + * contents of this table to it. + * + * @param tableFactory the table factory to use, must not be {@code null} + * @return a mutable copy of this table + */ + public Table newCopy(TableFactory tableFactory) { + Table t = tableFactory.newTable(); + for (Object key : entries.keySet()) { + Entry e = entries.get(key); + t.rawset(key, e.value); + } + return t; + } + + @Override + public boolean equals(Object o) { + if (this == o) + return true; + if (o == null || getClass() != o.getClass()) + return false; + ImmutableTable that = (ImmutableTable) o; + if (!this.entries.equals(that.entries)) + return false; + if (initialKey == null) { + if (that.initialKey != null) { + return false; + } + } else if (!initialKey.equals(that.initialKey)) { + return false; + } + return true; + } + + @Override + public int hashCode() { + int result = entries.hashCode(); + if (initialKey != null) { + result = 31 * result + initialKey.hashCode(); + } + return result; + } + + @Override + public Object rawget(Object key) { + key = Conversions.normaliseKey(key); + Entry e = entries.get(key); + return e != null ? e.value : null; + } + + /** + * Throws an {@link UnsupportedOperationException}, since this table is immutable. + * + * @param key ignored + * @param value ignored + * + * @throws UnsupportedOperationException every time this method is called + */ + @Override + public void rawset(Object key, Object value) { + throw new UnsupportedOperationException("table is immutable"); + } + + /** + * Throws an {@link UnsupportedOperationException}, since this table is immutable. + * + * @param idx ignored + * @param value ignored + * + * @throws UnsupportedOperationException every time this method is called + */ + @Override + public void rawset(long idx, Object value) { + throw new UnsupportedOperationException("table is immutable"); + } + + @Override + public Table getMetatable() { + return null; + } + + /** + * Throws an {@link UnsupportedOperationException}, since this table is immutable. + * + * @param mt ignored + * @return nothing (always throws an exception) + * + * @throws UnsupportedOperationException every time this method is called + */ + @Override + public Table setMetatable(Table mt) { + throw new UnsupportedOperationException("table is immutable"); + } + + @Override + public Object initialKey() { + return initialKey; + } + + @Override + public Object successorKeyOf(Object key) { + key = Conversions.normaliseKey(key); + try { + Entry e = entries.get(key); + return e.nextKey; + } catch (NullPointerException ex) { + throw new IllegalArgumentException("invalid key to 'next'", ex); + } + } + + @Override + protected void setMode(boolean weakKeys, boolean weakValues) { + // no-op + } + + /** + * Builder class for constructing instances of {@link ImmutableTable}. + */ + public static class Builder { + + private final TraversableHashMap entries; + + private static void checkKey(Object key) { + if (key == null || (key instanceof Double && Double.isNaN(((Double) key).doubleValue()))) { + throw new IllegalArgumentException( + "invalid table key: " + Conversions.toHumanReadableString(key)); + } + } + + private Builder(TraversableHashMap entries) { + this.entries = Objects.requireNonNull(entries); + } + + /** + * Constructs a new empty builder. + */ + public Builder() { + this(new TraversableHashMap<>()); + } + + private static TraversableHashMap mapCopy(TraversableHashMap map) { + TraversableHashMap result = new TraversableHashMap<>(); + result.putAll(map); + return result; + } + + /** + * Constructs a copy of the given builder (a copy constructor). + * + * @param builder the original builder, must not be {@code null} + * + * @throws NullPointerException if {@code builder} is {@code null} + */ + public Builder(Builder builder) { + this(mapCopy(builder.entries)); + } + + /** + * Sets the value associated with the key {@code key} to {@code value}. + * + *

+ * The behaviour of this method is similar to that of {@link Table#rawset(Object, Object)}: + *

+ *
    + *
  • when {@code value} is nil (i.e., {@code null}), the key {@code key} will not have + * any value associated with it after this method returns;
  • + *
  • nil and NaN keys are rejected by throwing a + * {@link IllegalArgumentException};
  • + *
  • numeric keys with an integer value are converted to that integer value.
  • + *
+ * + *

+ * The method returns {@code this}, allowing calls to this method to be chained. + *

+ * + * @param key the key, must not be {@code null} or NaN + * @param value the value, may be {@code null} + * @return this builder + * + * @throws IllegalArgumentException when {@code key} is {@code null} or a NaN + */ + public Builder add(Object key, Object value) { + key = Conversions.normaliseKey(key); + checkKey(key); + + if (value != null) { + entries.put(key, value); + } else { + entries.remove(key); + } + + return this; + } + + /** + * Clears the builder. + */ + public void clear() { + entries.clear(); + } + + /** + * Constructs and returns a new immutable table based on the contents of this builder. + * + * @return a new immutable table + */ + public ImmutableTable build() { + Map tableEntries = new HashMap<>(); + + for (Map.Entry e : entries.entrySet()) { + Object k = e.getKey(); + tableEntries.put(e.getKey(), new Entry(e.getValue(), entries.getSuccessorOf(k))); + } + return new ImmutableTable(Collections.unmodifiableMap(tableEntries), entries.getFirstKey()); + } + + } } diff --git a/rembulan-runtime/src/test/java/net/sandius/rembulan/impl/ImmutableTableTest.java b/rembulan-runtime/src/test/java/net/sandius/rembulan/impl/ImmutableTableTest.java new file mode 100644 index 000000000..d50da5c67 --- /dev/null +++ b/rembulan-runtime/src/test/java/net/sandius/rembulan/impl/ImmutableTableTest.java @@ -0,0 +1,57 @@ +package net.sandius.rembulan.impl; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; + +import java.util.HashMap; + +import org.junit.Test; + +public class ImmutableTableTest { + + @Test + public void test_equals_returns_true_for_both_empty_tables() { + // Given: + ImmutableTable underTest = new ImmutableTable.Builder().build(); + ImmutableTable other = new ImmutableTable.Builder().build(); + + // When: + boolean actual = underTest.equals(other); + + // Then: + assertTrue("actual", actual); + } + + @Test + public void test_hashCode_is_same_for_both_empty_tables() { + // Given: + ImmutableTable underTest = new ImmutableTable.Builder().build(); + ImmutableTable other = new ImmutableTable.Builder().build(); + + // When: + int actual = underTest.hashCode(); + int expected = other.hashCode(); + + // Then: + assertEquals("actual", expected, actual); + } + + @Test + public void test_can_add_empty_table_as_key_to_hashmap() { + // Given: + ImmutableTable underTest = new ImmutableTable.Builder().build(); + HashMap someMap = new HashMap<>(); + + // When: + Exception actual = null; + try { + someMap.put(underTest, "any value"); + } catch (Exception ex) { + actual = ex; + } + + // Then: + assertNull("actual", actual); + } +}