diff --git a/classlib/src/main/java/org/teavm/classlib/java/util/TEnumMap.java b/classlib/src/main/java/org/teavm/classlib/java/util/TEnumMap.java new file mode 100644 index 000000000..7bad40903 --- /dev/null +++ b/classlib/src/main/java/org/teavm/classlib/java/util/TEnumMap.java @@ -0,0 +1,264 @@ +/* + * Copyright 2017 Alexey Andreev. + * + * 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. + */ +package org.teavm.classlib.java.util; + +import java.io.Serializable; +import java.util.AbstractMap; +import java.util.AbstractSet; +import java.util.Arrays; +import java.util.Iterator; +import java.util.Map; +import java.util.NoSuchElementException; +import java.util.Objects; +import java.util.Set; + +public class TEnumMap, V> extends AbstractMap implements Serializable, Cloneable { + private Class keyType; + private Object[] data; + private boolean[] provided; + private int size; + private Set> entrySet; + + public TEnumMap(Class keyType) { + initFromKeyType(keyType); + } + + public TEnumMap(TEnumMap m) { + initFromOtherEnumMap(m); + } + + public TEnumMap(Map m) { + if (m instanceof TEnumMap) { + initFromOtherEnumMap((TEnumMap) m); + } else { + if (m.isEmpty()) { + throw new IllegalArgumentException(); + } + initFromKeyType(m.keySet().iterator().next().getDeclaringClass()); + for (Entry entry : m.entrySet()) { + int index = entry.getKey().ordinal(); + provided[index] = true; + data[index] = entry.getValue(); + } + size = m.size(); + } + } + + private void initFromKeyType(Class keyType) { + this.keyType = keyType; + this.data = new Object[TGenericEnumSet.getConstants(keyType).length]; + this.provided = new boolean[data.length]; + } + + private void initFromOtherEnumMap(TEnumMap m) { + this.keyType = m.keyType; + this.data = Arrays.copyOf(m.data, m.data.length); + this.provided = Arrays.copyOf(m.provided, m.provided.length); + this.size = m.size; + } + + @Override + public int size() { + return size; + } + + @Override + public boolean containsValue(Object value) { + for (int i = 0; i < data.length; ++i) { + if (provided[i] && Objects.equals(value, data[i])) { + return true; + } + } + return false; + } + + @Override + public boolean containsKey(Object key) { + if (!keyType.isInstance(key)) { + return false; + } + int index = ((Enum) key).ordinal(); + return provided[index]; + } + + @Override + public V get(Object key) { + if (!keyType.isInstance(key)) { + return null; + } + int index = ((Enum) key).ordinal(); + @SuppressWarnings("unchecked") + V value = (V) data[index]; + return value; + } + + @Override + public V put(K key, V value) { + int index = key.ordinal(); + @SuppressWarnings("unchecked") + V old = (V) data[index]; + if (!provided[index]) { + provided[index] = true; + size++; + } + data[index] = value; + return old; + } + + @Override + public V remove(Object key) { + if (!keyType.isInstance(key)) { + return null; + } + int index = ((Enum) key).ordinal(); + @SuppressWarnings("unchecked") + V old = (V) data[index]; + if (provided[index]) { + provided[index] = false; + data[index] = null; + size--; + } + return old; + } + + @Override + public void putAll(Map m) { + for (Map.Entry entry : m.entrySet()) { + int index = entry.getKey().ordinal(); + if (!provided[index]) { + provided[index] = true; + size++; + } + data[index] = entry.getValue(); + } + } + + @Override + public void clear() { + if (size > 0) { + size = 0; + Arrays.fill(provided, false); + Arrays.fill(data, null); + } + } + + @Override + public Set> entrySet() { + if (entrySet == null) { + entrySet = new AbstractSet>() { + @Override + public Iterator> iterator() { + return new Iterator>() { + int index; + int removeIndex = -1; + + { + find(); + } + + @Override + public boolean hasNext() { + return index < data.length; + } + + @Override + public Entry next() { + if (index >= data.length) { + throw new NoSuchElementException(); + } + removeIndex = index; + EntryImpl result = new EntryImpl(index++); + find(); + return result; + } + + private void find() { + while (index < provided.length && !provided[index]) { + index++; + } + } + + @Override + public void remove() { + if (removeIndex < 0) { + throw new IllegalStateException(); + } + data[removeIndex] = null; + provided[removeIndex] = false; + size--; + removeIndex = -1; + } + }; + } + + @Override + public int size() { + return size; + } + + @Override + public boolean remove(Object o) { + if (!keyType.isInstance(o)) { + return false; + } + int index = ((Enum) o).ordinal(); + if (provided[index]) { + provided[index] = true; + data[index] = null; + size--; + return true; + } else { + return false; + } + } + + @Override + public void clear() { + TEnumMap.this.clear(); + } + + class EntryImpl implements Entry { + int index; + + EntryImpl(int index) { + this.index = index; + } + + @Override + @SuppressWarnings("unchecked") + public K getKey() { + return (K) TGenericEnumSet.getConstants(keyType)[index]; + } + + @Override + @SuppressWarnings("unchecked") + public V getValue() { + return (V) data[index]; + } + + @Override + public V setValue(V value) { + @SuppressWarnings("unchecked") + V old = (V) data[index]; + data[index] = value; + return old; + } + } + }; + } + return entrySet; + } +} diff --git a/tests/src/test/java/org/teavm/classlib/java/util/EnumMapTest.java b/tests/src/test/java/org/teavm/classlib/java/util/EnumMapTest.java new file mode 100644 index 000000000..01ef05b2c --- /dev/null +++ b/tests/src/test/java/org/teavm/classlib/java/util/EnumMapTest.java @@ -0,0 +1,234 @@ +/* + * Copyright 2017 Alexey Andreev. + * + * 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. + */ +package org.teavm.classlib.java.util; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.EnumMap; +import java.util.HashMap; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.teavm.junit.TeaVMTestRunner; + +@RunWith(TeaVMTestRunner.class) +public class EnumMapTest { + @Test + public void emptyCreated() { + EnumMap map = new EnumMap<>(L.class); + assertEquals(0, map.size()); + assertFalse(map.entrySet().iterator().hasNext()); + assertNull(map.get(L.A)); + assertFalse(map.containsKey(L.A)); + } + + @Test + public void createdFromOtherEnumMap() { + EnumMap map = new EnumMap<>(L.class); + assertNull(map.put(L.A, "A")); + + EnumMap otherMap = new EnumMap<>(map); + map.clear(); + assertEquals(1, otherMap.size()); + assertEquals("A", otherMap.get(L.A)); + + otherMap = new EnumMap<>((Map) map); + assertEquals(0, otherMap.size()); + assertEquals(null, otherMap.get(L.A)); + } + + @Test + public void createdFromOtherMap() { + Map map = new HashMap<>(); + assertNull(map.put(L.A, "A")); + + EnumMap otherMap = new EnumMap<>(map); + map.clear(); + assertEquals(1, otherMap.size()); + assertEquals("A", otherMap.get(L.A)); + + try { + new EnumMap<>(map); + fail("Should throw exception when creating from empty map"); + } catch (IllegalArgumentException e) { + // expected + } + } + + @Test + public void entriesAdded() { + EnumMap map = new EnumMap<>(L.class); + assertNull(map.put(L.A, "A")); + assertEquals(1, map.size()); + assertEquals("A", map.get(L.A)); + assertTrue(map.containsKey(L.A)); + + assertEquals("A", map.put(L.A, "A0")); + assertEquals(1, map.size()); + assertEquals("A0", map.get(L.A)); + assertTrue(map.containsKey(L.A)); + + assertNull(map.put(L.B, "B")); + assertEquals(2, map.size()); + assertEquals("B", map.get(L.B)); + assertTrue(map.containsKey(L.B)); + + List values = new ArrayList<>(); + List keys = new ArrayList<>(); + for (Map.Entry entry : map.entrySet()) { + values.add(entry.getValue()); + keys.add(entry.getKey()); + } + assertEquals(Arrays.asList("A0", "B"), values); + assertEquals(Arrays.asList(L.A, L.B), keys); + } + + @Test + public void multipleEntriesAdded() { + EnumMap map = new EnumMap<>(L.class); + map.put(L.A, "A"); + map.put(L.B, "B"); + + Map otherMap = new HashMap<>(); + otherMap.put(L.B, "B0"); + otherMap.put(L.C, "C0"); + map.putAll(otherMap); + + assertEquals(3, map.size()); + assertEquals("A", map.get(L.A)); + assertEquals("B0", map.get(L.B)); + assertEquals("C0", map.get(L.C)); + } + + @Test + public void entriesRemoved() { + EnumMap map = new EnumMap<>(L.class); + map.put(L.A, "A"); + map.put(L.B, "B"); + assertEquals(2, map.size()); + + assertEquals("A", map.remove(L.A)); + assertEquals(1, map.size()); + assertNull(map.get(L.A)); + + assertNull(map.remove(L.A)); + assertEquals(1, map.size()); + + assertNull(map.remove("Dummy")); + + List values = new ArrayList<>(); + List keys = new ArrayList<>(); + for (Map.Entry entry : map.entrySet()) { + values.add(entry.getValue()); + keys.add(entry.getKey()); + } + assertEquals(Arrays.asList("B"), values); + assertEquals(Arrays.asList(L.B), keys); + } + + @Test + public void containsNullValue() { + EnumMap map = new EnumMap<>(L.class); + map.put(L.A, null); + assertEquals(1, map.size()); + assertNull(map.get(L.A)); + assertTrue(map.containsKey(L.A)); + assertNull(map.values().iterator().next()); + assertEquals(L.A, map.keySet().iterator().next()); + } + + @Test + public void clearWorks() { + EnumMap map = new EnumMap<>(L.class); + map.put(L.A, "A"); + map.put(L.B, "B"); + assertEquals(2, map.size()); + + map.clear(); + assertEquals(0, map.size()); + assertFalse(map.entrySet().iterator().hasNext()); + assertFalse(map.containsKey(L.A)); + assertNull(map.get(L.A)); + } + + @Test + public void iteratorReplacesValue() { + EnumMap map = new EnumMap<>(L.class); + map.put(L.A, "A"); + map.put(L.B, "B"); + + Iterator> iter = map.entrySet().iterator(); + assertTrue(iter.hasNext()); + Map.Entry entry = iter.next(); + assertEquals(L.A, entry.getKey()); + assertEquals("A", entry.setValue("A0")); + assertTrue(iter.hasNext()); + entry = iter.next(); + assertEquals(L.B, entry.getKey()); + assertEquals("B", entry.setValue("B0")); + assertFalse(iter.hasNext()); + + assertEquals("A0", map.get(L.A)); + assertEquals("B0", map.get(L.B)); + } + + @Test + public void iteratorRemovesValue() { + EnumMap map = new EnumMap<>(L.class); + map.put(L.A, "A"); + map.put(L.B, "B"); + + Iterator> iter = map.entrySet().iterator(); + + try { + iter.remove(); + fail("Remove without calling next should throw exception"); + } catch (IllegalStateException e) { + // It's expected + } + + assertTrue(iter.hasNext()); + Map.Entry entry = iter.next(); + assertEquals(L.A, entry.getKey()); + iter.remove(); + + try { + iter.remove(); + fail("Repeated remove should throw exception"); + } catch (IllegalStateException e) { + // It's expected + } + + assertTrue(iter.hasNext()); + iter.next(); + assertFalse(iter.hasNext()); + + assertEquals(null, map.get(L.A)); + assertEquals("B", map.get(L.B)); + assertEquals(1, map.size()); + } + + enum L { + A, B, C + } +}