Repository: graphql-java-kickstart/graphql-java-tools (kotlin)

Issue:
Remove Guava dependency
Guava is a big library (2.7 MB), and the dependency of graphql-java-tools on Guava is quite loose, as pointed out already in #283. 

There are only 2 functionalities used from Guava:
- Primitives::wrap - wraps a primitive (like int.class) to the corresponding class (e.g. Integer.class). This can be trivially replaced.
- BiMap - used to back the type dictionary. It can also be easily replaced.

Removing Guava is desirable for projects that use graphql-java-tools because:
- they may not need Guava at all, since they rely on other open source libraries with similar features (e.g. appache-commons-*)
- they may not want the headache of managing different versions of the Guava library. For example, graphql-java-tools uses v26.0, graphql-java-servlet uses 24.1.1 and the latest version is 28.2 
 
I'll provide a pull request that removes the Guava dependency.

Pull request description:
Provide replacement implementations for:
 - BiMap
 - Primitives::wrap

Fixes #374

Patch:
diff --git a/pom.xml b/pom.xml
index 36dc7641..c819594a 100644
--- a/pom.xml
+++ b/pom.xml
@@ -114,11 +114,6 @@
             <artifactId>commons-lang3</artifactId>
             <version>3.9</version>
         </dependency>
-        <dependency>
-            <groupId>com.google.guava</groupId>
-            <artifactId>guava</artifactId>
-            <version>26.0-jre</version>
-        </dependency>
         <dependency>
             <groupId>org.slf4j</groupId>
             <artifactId>slf4j-api</artifactId>
diff --git a/src/main/kotlin/graphql/kickstart/tools/DictionaryTypeResolver.kt b/src/main/kotlin/graphql/kickstart/tools/DictionaryTypeResolver.kt
index f1aa6584..18f15324 100644
--- a/src/main/kotlin/graphql/kickstart/tools/DictionaryTypeResolver.kt
+++ b/src/main/kotlin/graphql/kickstart/tools/DictionaryTypeResolver.kt
@@ -1,7 +1,7 @@
 package graphql.kickstart.tools
 
-import com.google.common.collect.BiMap
 import graphql.TypeResolutionEnvironment
+import graphql.kickstart.tools.util.BiMap
 import graphql.language.TypeDefinition
 import graphql.schema.GraphQLInterfaceType
 import graphql.schema.GraphQLObjectType
diff --git a/src/main/kotlin/graphql/kickstart/tools/GenericType.kt b/src/main/kotlin/graphql/kickstart/tools/GenericType.kt
index 7771c5fd..b144f7b8 100644
--- a/src/main/kotlin/graphql/kickstart/tools/GenericType.kt
+++ b/src/main/kotlin/graphql/kickstart/tools/GenericType.kt
@@ -1,7 +1,7 @@
 package graphql.kickstart.tools
 
 import com.fasterxml.classmate.ResolvedType
-import com.google.common.primitives.Primitives
+import graphql.kickstart.tools.util.Primitives
 import org.apache.commons.lang3.reflect.TypeUtils
 import java.lang.reflect.ParameterizedType
 import java.lang.reflect.TypeVariable
diff --git a/src/main/kotlin/graphql/kickstart/tools/ScannedSchemaObjects.kt b/src/main/kotlin/graphql/kickstart/tools/ScannedSchemaObjects.kt
index be6f696d..7f110266 100644
--- a/src/main/kotlin/graphql/kickstart/tools/ScannedSchemaObjects.kt
+++ b/src/main/kotlin/graphql/kickstart/tools/ScannedSchemaObjects.kt
@@ -1,6 +1,6 @@
 package graphql.kickstart.tools
 
-import com.google.common.collect.BiMap
+import graphql.kickstart.tools.util.BiMap
 import graphql.language.FieldDefinition
 import graphql.language.ObjectTypeDefinition
 import graphql.language.TypeDefinition
diff --git a/src/main/kotlin/graphql/kickstart/tools/SchemaClassScanner.kt b/src/main/kotlin/graphql/kickstart/tools/SchemaClassScanner.kt
index 82b59634..f1594326 100644
--- a/src/main/kotlin/graphql/kickstart/tools/SchemaClassScanner.kt
+++ b/src/main/kotlin/graphql/kickstart/tools/SchemaClassScanner.kt
@@ -1,8 +1,6 @@
 package graphql.kickstart.tools
 
-import com.google.common.collect.BiMap
-import com.google.common.collect.HashBiMap
-import com.google.common.collect.Maps
+import graphql.kickstart.tools.util.BiMap
 import graphql.language.*
 import graphql.schema.GraphQLScalarType
 import graphql.schema.idl.ScalarInfo
@@ -120,7 +118,7 @@ internal class SchemaClassScanner(initialDictionary: BiMap<String, Class<*>>, al
         // Input types can also be excluded from the dictionary, since it's only used for interfaces, unions, and enums.
         // Union types can also be excluded, as their possible types are resolved recursively later
         val dictionary = try {
-            Maps.unmodifiableBiMap(HashBiMap.create<TypeDefinition<*>, JavaType>().also {
+            BiMap.unmodifiableBiMap(BiMap.create<TypeDefinition<*>, JavaType>().also {
                 dictionary.filter {
                     it.value.javaType != null
                             && it.value.typeClass() != java.lang.Object::class.java
diff --git a/src/main/kotlin/graphql/kickstart/tools/SchemaParserBuilder.kt b/src/main/kotlin/graphql/kickstart/tools/SchemaParserBuilder.kt
index f2d4d358..f8e2b2bd 100644
--- a/src/main/kotlin/graphql/kickstart/tools/SchemaParserBuilder.kt
+++ b/src/main/kotlin/graphql/kickstart/tools/SchemaParserBuilder.kt
@@ -1,10 +1,8 @@
 package graphql.kickstart.tools
 
 import com.fasterxml.jackson.databind.ObjectMapper
-import com.google.common.collect.BiMap
-import com.google.common.collect.HashBiMap
-import com.google.common.collect.Maps
 import graphql.kickstart.tools.relay.RelayConnectionFactory
+import graphql.kickstart.tools.util.BiMap
 import graphql.language.Definition
 import graphql.language.Document
 import graphql.parser.Parser
@@ -216,9 +214,9 @@ class InvalidSchemaError(pce: ParseCancellationException, private val recognitio
 
 class SchemaParserDictionary {
 
-    private val dictionary: BiMap<String, Class<*>> = HashBiMap.create()
+    private val dictionary: BiMap<String, Class<*>> = BiMap.create()
 
-    fun getDictionary(): BiMap<String, Class<*>> = Maps.unmodifiableBiMap(dictionary)
+    fun getDictionary(): BiMap<String, Class<*>> = BiMap.unmodifiableBiMap(dictionary)
 
     /**
      * Add arbitrary classes to the parser's dictionary, overriding the generated type name.
diff --git a/src/main/kotlin/graphql/kickstart/tools/util/BiMap.java b/src/main/kotlin/graphql/kickstart/tools/util/BiMap.java
new file mode 100644
index 00000000..f289c667
--- /dev/null
+++ b/src/main/kotlin/graphql/kickstart/tools/util/BiMap.java
@@ -0,0 +1,127 @@
+package graphql.kickstart.tools.util;
+
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Set;
+
+import static java.util.Collections.unmodifiableMap;
+
+@SuppressWarnings({"NullableProblems", "SuspiciousMethodCalls"})
+public class BiMap<K, V> implements Map<K, V> {
+    private final Map<K, V> delegate;
+    private final Map<V, K> inverse;
+
+    public static <K, V> BiMap<K, V> create() {
+        return new BiMap<>(new HashMap<>(), new HashMap<>());
+    }
+
+    public static <K, V> BiMap<K, V> unmodifiableBiMap(BiMap<K, V> biMap) {
+        return new BiMap<>(unmodifiableMap(biMap.delegate), unmodifiableMap(biMap.inverse));
+    }
+
+    private BiMap(Map<K, V> delegate, Map<V, K> inverse) {
+        this.delegate = delegate;
+        this.inverse = inverse;
+    }
+
+    public BiMap<V, K> inverse() {
+        return new BiMap<>(inverse, delegate);
+    }
+
+    @Override
+    public void clear() {
+        delegate.clear();
+        inverse.clear();
+    }
+
+    @Override
+    public boolean containsKey(Object key) {
+        return delegate.containsKey(key);
+    }
+
+    @Override
+    public boolean containsValue(Object value) {
+        return inverse.containsKey(value);
+    }
+
+    @Override
+    public Set<Entry<K, V>> entrySet() {
+        return delegate.entrySet();
+    }
+
+    @Override
+    public V get(Object key) {
+        return delegate.get(key);
+    }
+
+    @Override
+    public boolean isEmpty() {
+        return delegate.isEmpty();
+    }
+
+    @Override
+    public Set<K> keySet() {
+        return delegate.keySet();
+    }
+
+    /**
+     * {@inheritDoc}
+     *
+     * @throws IllegalArgumentException if the given value is already bound to a different key in this bimap.
+     */
+    @Override
+    public V put(K key, V value) {
+        // if the key already exists, and the value is the same as the old, then nothing to do
+        boolean containedKey = containsKey(key);
+        if (containedKey && Objects.equals(value, get(key))) {
+            return value;
+        }
+
+        // if the value already exists, then it's not ok
+        if (containsValue(value)) {
+            throw new IllegalArgumentException("value already present: " + value);
+        }
+
+        // put the value; if the key already exists, it replaces an existing value, and we have to remove it from the inverse as well
+        V oldValue = delegate.put(key, value);
+        if (containedKey) {
+            inverse.remove(oldValue);
+        }
+        inverse.put(value, key);
+
+        return oldValue;
+    }
+
+    /**
+     * {@inheritDoc}
+     *
+     * @throws IllegalArgumentException if an attempt to {@code put} any entry fails.
+     */
+    @Override
+    public void putAll(Map<? extends K, ? extends V> m) {
+        for (Entry<? extends K, ? extends V> entry : m.entrySet()) {
+            put(entry.getKey(), entry.getValue());
+        }
+    }
+
+    @Override
+    public V remove(Object key) {
+        if (!containsKey(key)) {
+            return null;
+        }
+
+        inverse.remove(delegate.get(key));
+        return delegate.remove(key);
+    }
+
+    @Override
+    public int size() {
+        return delegate.size();
+    }
+
+    @Override
+    public Set<V> values() {
+        return inverse.keySet();
+    }
+}
diff --git a/src/main/kotlin/graphql/kickstart/tools/util/Primitives.java b/src/main/kotlin/graphql/kickstart/tools/util/Primitives.java
new file mode 100644
index 00000000..0bf7c5d9
--- /dev/null
+++ b/src/main/kotlin/graphql/kickstart/tools/util/Primitives.java
@@ -0,0 +1,28 @@
+package graphql.kickstart.tools.util;
+
+public final class Primitives {
+    @SuppressWarnings("unchecked")
+    public static <T> Class<T> wrap(Class<T> type) {
+        if (boolean.class.equals(type)) {
+            return (Class<T>) Boolean.class;
+        } else if (byte.class.equals(type)) {
+            return (Class<T>) Byte.class;
+        } else if (char.class.equals(type)) {
+            return (Class<T>) Character.class;
+        } else if (double.class.equals(type)) {
+            return (Class<T>) Double.class;
+        } else if (float.class.equals(type)) {
+            return (Class<T>) Float.class;
+        } else if (int.class.equals(type)) {
+            return (Class<T>) Integer.class;
+        } else if (long.class.equals(type)) {
+            return (Class<T>) Long.class;
+        } else if (short.class.equals(type)) {
+            return (Class<T>) Short.class;
+        } else if (void.class.equals(type)) {
+            return (Class<T>) Void.class;
+        } else {
+            return type;
+        }
+    }
+}


Test patch:
diff --git a/src/test/java/graphql/kickstart/tools/util/BiMapTest.java b/src/test/java/graphql/kickstart/tools/util/BiMapTest.java
new file mode 100644
index 00000000..1d5d1893
--- /dev/null
+++ b/src/test/java/graphql/kickstart/tools/util/BiMapTest.java
@@ -0,0 +1,79 @@
+package graphql.kickstart.tools.util;
+
+import org.junit.Before;
+import org.junit.Test;
+
+import java.util.Arrays;
+import java.util.HashSet;
+import java.util.Set;
+
+import static org.hamcrest.CoreMatchers.is;
+import static org.junit.Assert.*;
+
+public class BiMapTest {
+    private BiMap<String, Integer> bimap;
+
+    @Before
+    public void setUp() {
+        bimap = BiMap.create();
+    }
+
+    @Test(expected = IllegalArgumentException.class)
+    public void testDoesNotPermitDuplicateValues() {
+        bimap.put("foo", 1);
+        bimap.put("bar", 1);
+    }
+
+    @Test
+    @SuppressWarnings("OverwrittenKey")
+    public void testPutSameKeySameValue() {
+        bimap.put("foo", 1);
+
+        // should not fail
+        bimap.put("foo", 1);
+    }
+
+    @Test
+    @SuppressWarnings({"ConstantConditions"})
+    public void testPutSameKeyNewValue() {
+        bimap.put("foo", 1);
+        bimap.put("bar", 2);
+
+        int old = bimap.put("bar", 3);
+
+        // old value should have been returned
+        assertEquals(2, old);
+
+        // inverse should have correct keys
+        Set<Integer> expected = new HashSet<>(Arrays.asList(1, 3));
+        assertEquals(expected, bimap.values());
+        assertEquals(expected, bimap.inverse().keySet());
+    }
+
+    @Test
+    public void testInverse() {
+        bimap.put("foo", 1);
+        bimap.put("bar", 2);
+        bimap.put("baz", 3);
+
+        BiMap<Integer, String> inverse = bimap.inverse();
+        assertTrue(inverse.containsKey(1));
+        assertTrue(inverse.containsKey(2));
+        assertTrue(inverse.containsKey(3));
+
+        assertThat(inverse.get(1), is("foo"));
+        assertThat(inverse.get(2), is("bar"));
+        assertThat(inverse.get(3), is("baz"));
+    }
+
+    @Test
+    public void testValues() {
+        bimap.put("foo", 1);
+        bimap.put("bar", 2);
+        bimap.put("baz", 3);
+
+        Set<Integer> values = bimap.values();
+        Set<Integer> expected = new HashSet<>(Arrays.asList(1, 2, 3));
+        assertThat(values, is(expected));
+    }
+}
diff --git a/src/test/java/graphql/kickstart/tools/util/PrimitivesTest.java b/src/test/java/graphql/kickstart/tools/util/PrimitivesTest.java
new file mode 100644
index 00000000..8cb29991
--- /dev/null
+++ b/src/test/java/graphql/kickstart/tools/util/PrimitivesTest.java
@@ -0,0 +1,29 @@
+package graphql.kickstart.tools.util;
+
+import org.junit.Test;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNull;
+
+public class PrimitivesTest {
+    @Test
+    public void testWrapPrimitive() {
+        Class<?> clazz = Primitives.wrap(int.class);
+
+        assertEquals(Integer.class, clazz);
+    }
+
+    @Test
+    public void testWrapNonPrimitive() {
+        Class<?> clazz = Primitives.wrap(String.class);
+
+        assertEquals(String.class, clazz);
+    }
+
+    @Test
+    public void testWrapNull() {
+        Class<?> clazz = Primitives.wrap(null);
+
+        assertNull(clazz);
+    }
+}